diff options
author | Massimiliano Culpo <massimiliano.culpo@gmail.com> | 2020-11-17 20:25:13 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-17 11:25:13 -0800 |
commit | 5f636fc3177a1ad5d5a2bc1a1471e6fa23106d99 (patch) | |
tree | 2291442db370659f098472e33e7d68d0536455df | |
parent | 7ffad278d34bcc1bda7d5d355e1104a10a7c484a (diff) | |
download | spack-5f636fc3177a1ad5d5a2bc1a1471e6fa23106d99.tar.gz spack-5f636fc3177a1ad5d5a2bc1a1471e6fa23106d99.tar.bz2 spack-5f636fc3177a1ad5d5a2bc1a1471e6fa23106d99.tar.xz spack-5f636fc3177a1ad5d5a2bc1a1471e6fa23106d99.zip |
spack containerize: allow users to customize the base image (#15028)
This PR reworks a few attributes in the container subsection of
spack.yaml to permit the injection of custom base images when
generating containers with Spack. In more detail, users can still
specify the base operating system and Spack version they want to use:
spack:
container:
images:
os: ubuntu:18.04
spack: develop
in which case the generated recipe will use one of the Spack images
built on Docker Hub for the build stage and the base OS image in the
final stage. Alternatively, they can specify explicitly the two
base images:
spack:
container:
images:
build: spack/ubuntu-bionic:latest
final: ubuntu:18.04
and it will be up to them to ensure their consistency.
Additional changes:
* This commit adds documentation on the two approaches.
* Users can now specify OS packages to install (e.g. with apt or yum)
prior to the build (previously this was only available for the
finalized image).
* Handles to avoid an update of the available system packages have been
added to the configuration to facilitate the generation of recipes
permitting deterministic builds.
-rw-r--r-- | lib/spack/docs/containers.rst | 423 | ||||
-rw-r--r-- | lib/spack/spack/container/__init__.py | 4 | ||||
-rw-r--r-- | lib/spack/spack/container/images.json | 75 | ||||
-rw-r--r-- | lib/spack/spack/container/images.py | 29 | ||||
-rw-r--r-- | lib/spack/spack/container/writers/__init__.py | 74 | ||||
-rw-r--r-- | lib/spack/spack/schema/container.py | 66 | ||||
-rw-r--r-- | lib/spack/spack/tengine.py | 4 | ||||
-rw-r--r-- | lib/spack/spack/test/container/conftest.py | 4 | ||||
-rw-r--r-- | lib/spack/spack/test/container/docker.py | 12 | ||||
-rw-r--r-- | lib/spack/spack/test/container/images.py | 3 | ||||
-rw-r--r-- | lib/spack/spack/test/container/schema.py | 4 | ||||
-rw-r--r-- | share/spack/templates/container/Dockerfile | 20 | ||||
-rw-r--r-- | share/spack/templates/container/singularity.def | 34 |
13 files changed, 514 insertions, 238 deletions
diff --git a/lib/spack/docs/containers.rst b/lib/spack/docs/containers.rst index 2590fec8b6..f5bd5602bc 100644 --- a/lib/spack/docs/containers.rst +++ b/lib/spack/docs/containers.rst @@ -9,28 +9,48 @@ Container Images ================ -Spack can be an ideal tool to setup images for containers since all the -features discussed in :ref:`environments` can greatly help to manage -the installation of software during the image build process. Nonetheless, -building a production image from scratch still requires a lot of -boilerplate to: +Spack :ref:`environments` are a great tool to create container images, but +preparing one that is suitable for production requires some more boilerplate +than just: -- Get Spack working within the image, possibly running as root -- Minimize the physical size of the software installed -- Properly update the system software in the base image +.. code-block:: docker + + COPY spack.yaml /environment + RUN spack -e /environment install + +Additional actions may be needed to minimize the size of the +container, or to update the system software that is installed in the base +image, or to set up a proper entrypoint to run the image. These tasks are +usually both necessary and repetitive, so Spack comes with a command +to generate recipes for container images starting from a ``spack.yaml``. + +-------------------- +A Quick Introduction +-------------------- + +Consider having a Spack environment like the following: + +.. code-block:: yaml + + spack: + specs: + - gromacs+mpi + - mpich -To facilitate users with these tedious tasks, Spack provides a command -to automatically generate recipes for container images based on -Environments: +Producing a ``Dockerfile`` from it is as simple as moving to the directory +where the ``spack.yaml`` file is stored and giving the following command: .. code-block:: console - $ ls - spack.yaml + $ spack containerize > Dockerfile + +The ``Dockerfile`` that gets created uses multi-stage builds and +other techniques to minimize the size of the final image: + +.. code-block:: docker - $ spack containerize # Build stage with Spack pre-installed and ready to be used - FROM spack/centos7:latest as builder + FROM spack/ubuntu-bionic:latest as builder # What we want to install and how we want to install it # is specified in a manifest file (spack.yaml) @@ -45,7 +65,7 @@ Environments: && echo " view: /opt/view") > /opt/spack-environment/spack.yaml # Install the software, remove unnecessary deps - RUN cd /opt/spack-environment && spack env activate . && spack install && spack gc -y + RUN cd /opt/spack-environment && spack env activate . && spack install --fail-fast && spack gc -y # Strip all the binaries RUN find -L /opt/view/* -type f -exec readlink -f '{}' \; | \ @@ -58,45 +78,34 @@ Environments: RUN cd /opt/spack-environment && \ spack env activate --sh -d . >> /etc/profile.d/z10_spack_environment.sh - # Bare OS image to run the installed executables - FROM centos:7 + FROM ubuntu:18.04 COPY --from=builder /opt/spack-environment /opt/spack-environment COPY --from=builder /opt/software /opt/software COPY --from=builder /opt/view /opt/view COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh - RUN yum update -y && yum install -y epel-release && yum update -y \ - && yum install -y libgomp \ - && rm -rf /var/cache/yum && yum clean all - - RUN echo 'export PS1="\[$(tput bold)\]\[$(tput setaf 1)\][gromacs]\[$(tput setaf 2)\]\u\[$(tput sgr0)\]:\w $ "' >> ~/.bashrc - - - LABEL "app"="gromacs" - LABEL "mpi"="mpich" - ENTRYPOINT ["/bin/bash", "--rcfile", "/etc/profile", "-l"] -In order to build and run the image, execute: +The image itself can then be built and run in the usual way, with any of the +tools suitable for the task. For instance, if we decided to use ``docker``: .. code-block:: bash $ spack containerize > Dockerfile $ docker build -t myimage . + [ ... ] $ docker run -it myimage -The bits that make this automation possible are discussed in details -below. All the images generated in this way will be based on -multi-stage builds with: +The various components involved in the generation of the recipe and their +configuration are discussed in details in the sections below. -- A fat ``build`` stage containing common build tools and Spack itself -- A minimal ``final`` stage containing only the software requested by the user +.. _container_spack_images: ------------------ -Spack Base Images ------------------ +-------------------------- +Spack Images on Docker Hub +-------------------------- Docker images with Spack preinstalled and ready to be used are built on `Docker Hub <https://hub.docker.com/u/spack>`_ @@ -131,19 +140,20 @@ All the images are tagged with the corresponding release of Spack: with the exception of the ``latest`` tag that points to the HEAD of the ``develop`` branch. These images are available for anyone to use and take care of all the repetitive tasks that are necessary -to setup Spack within a container. All the container recipes generated -automatically by Spack use them as base images for their ``build`` stage. - +to setup Spack within a container. The container recipes generated +by Spack use them as default base images for their ``build`` stage, +even though handles to use custom base images provided by users are +available to accommodate complex use cases. -------------------------- -Environment Configuration -------------------------- +--------------------------------- +Creating Images From Environments +--------------------------------- Any Spack Environment can be used for the automatic generation of container recipes. Sensible defaults are provided for things like the base image or the -version of Spack used in the image. If a finer tuning is needed it can be -obtained by adding the relevant metadata under the ``container`` attribute -of environments: +version of Spack used in the image. +If a finer tuning is needed it can be obtained by adding the relevant metadata +under the ``container`` attribute of environments: .. code-block:: yaml @@ -157,9 +167,10 @@ of environments: # singularity or anything else that is currently supported format: docker - # Select from a valid list of images - base: - image: "centos:7" + # Sets the base images for the stages where Spack builds the + # software or where the software gets installed after being built.. + images: + os: "centos:7" spack: develop # Whether or not to strip binaries @@ -167,7 +178,8 @@ of environments: # Additional system packages that are needed at runtime os_packages: - - libgomp + final: + - libgomp # Extra instructions extra_instructions: @@ -179,7 +191,210 @@ of environments: app: "gromacs" mpi: "mpich" -The tables below describe the configuration options that are currently supported: +A detailed description of the options available can be found in the +:ref:`container_config_options` section. + +------------------- +Setting Base Images +------------------- + +The ``images`` subsection is used to select both the image where +Spack builds the software and the image where the built software +is installed. This attribute can be set in two different ways and +which one to use depends on the use case at hand. + +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Use Official Spack Images From Dockerhub +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To generate a recipe that uses an official Docker image from the +Spack organization to build the software and the corresponding official OS image +to install the built software, all the user has to do is specify: + +1. An operating system under ``images:os`` +2. A Spack version under ``images:spack`` + +Any combination of these two values that can be mapped to one of the images +discussed in :ref:`container_spack_images` is allowed. For instance, the +following ``spack.yaml``: + +.. code-block:: yaml + + spack: + specs: + - gromacs+mpi + - mpich + + container: + images: + os: centos/7 + spack: 0.15.4 + +uses ``spack/centos7:0.15.4`` and ``centos:7`` for the stages where the +software is respectively built and installed: + +.. code-block:: docker + + # Build stage with Spack pre-installed and ready to be used + FROM spack/centos7:0.15.4 as builder + + # What we want to install and how we want to install it + # is specified in a manifest file (spack.yaml) + RUN mkdir /opt/spack-environment \ + && (echo "spack:" \ + && echo " specs:" \ + && echo " - gromacs+mpi" \ + && echo " - mpich" \ + && echo " concretization: together" \ + && echo " config:" \ + && echo " install_tree: /opt/software" \ + && echo " view: /opt/view") > /opt/spack-environment/spack.yaml + [ ... ] + # Bare OS image to run the installed executables + FROM centos:7 + + COPY --from=builder /opt/spack-environment /opt/spack-environment + COPY --from=builder /opt/software /opt/software + COPY --from=builder /opt/view /opt/view + COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh + + ENTRYPOINT ["/bin/bash", "--rcfile", "/etc/profile", "-l"] + +This method of selecting base images is the simplest of the two, and we advise +to use it whenever possible. There are cases though where using Spack official +images is not enough to fit production needs. In these situations users can manually +select which base image to start from in the recipe, as we'll see next. + +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Use Custom Images Provided by Users +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Consider, as an example, building a production grade image for a CUDA +application. The best strategy would probably be to build on top of +images provided by the vendor and regard CUDA as an external package. + +Spack doesn't currently provide an official image with CUDA configured +this way, but users can build it on their own and then configure the +environment to explicitly pull it. This requires users to: + +1. Specify the image used to build the software under ``images:build`` +2. Specify the image used to install the built software under ``images:final`` + +A ``spack.yaml`` like the following: + +.. code-block:: yaml + + spack: + specs: + - gromacs@2019.4+cuda build_type=Release + - mpich + - fftw precision=float + packages: + cuda: + buildable: False + externals: + - spec: cuda%gcc + prefix: /usr/local/cuda + + container: + images: + build: custom/cuda-10.1-ubuntu18.04:latest + final: nvidia/cuda:10.1-base-ubuntu18.04 + +produces, for instance, the following ``Dockerfile``: + +.. code-block:: docker + + # Build stage with Spack pre-installed and ready to be used + FROM custom/cuda-10.1-ubuntu18.04:latest as builder + + # What we want to install and how we want to install it + # is specified in a manifest file (spack.yaml) + RUN mkdir /opt/spack-environment \ + && (echo "spack:" \ + && echo " specs:" \ + && echo " - gromacs@2019.4+cuda build_type=Release" \ + && echo " - mpich" \ + && echo " - fftw precision=float" \ + && echo " packages:" \ + && echo " cuda:" \ + && echo " buildable: false" \ + && echo " externals:" \ + && echo " - spec: cuda%gcc" \ + && echo " prefix: /usr/local/cuda" \ + && echo " concretization: together" \ + && echo " config:" \ + && echo " install_tree: /opt/software" \ + && echo " view: /opt/view") > /opt/spack-environment/spack.yaml + + # Install the software, remove unnecessary deps + RUN cd /opt/spack-environment && spack env activate . && spack install --fail-fast && spack gc -y + + # Strip all the binaries + RUN find -L /opt/view/* -type f -exec readlink -f '{}' \; | \ + xargs file -i | \ + grep 'charset=binary' | \ + grep 'x-executable\|x-archive\|x-sharedlib' | \ + awk -F: '{print $1}' | xargs strip -s + + # Modifications to the environment that are necessary to run + RUN cd /opt/spack-environment && \ + spack env activate --sh -d . >> /etc/profile.d/z10_spack_environment.sh + + # Bare OS image to run the installed executables + FROM nvidia/cuda:10.1-base-ubuntu18.04 + + COPY --from=builder /opt/spack-environment /opt/spack-environment + COPY --from=builder /opt/software /opt/software + COPY --from=builder /opt/view /opt/view + COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh + + ENTRYPOINT ["/bin/bash", "--rcfile", "/etc/profile", "-l"] + +where the base images for both stages are completely custom. + +This second mode of selection for base images is more flexible than just +choosing an operating system and a Spack version, but is also more demanding. +Users may need to generate by themselves their base images and it's also their +responsibility to ensure that: + +1. Spack is available in the ``build`` stage and set up correctly to install the required software +2. The artifacts produced in the ``build`` stage can be executed in the ``final`` stage + +Therefore we don't recommend its use in cases that can be otherwise +covered by the simplified mode shown first. + +---------------------------- +Singularity Definition Files +---------------------------- + +In addition to producing recipes in ``Dockerfile`` format Spack can produce +Singularity Definition Files by just changing the value of the ``format`` +attribute: + +.. code-block:: console + + $ cat spack.yaml + spack: + specs: + - hdf5~mpi + container: + format: singularity + + $ spack containerize > hdf5.def + $ sudo singularity build hdf5.sif hdf5.def + +The minimum version of Singularity required to build a SIF (Singularity Image Format) +image from the recipes generated by Spack is ``3.5.3``. + +.. _container_config_options: + +----------------------- +Configuration Reference +----------------------- + +The tables below describe all the configuration options that are currently supported +to customize the generation of container recipes: .. list-table:: General configuration options for the ``container`` section of ``spack.yaml`` :header-rows: 1 @@ -192,21 +407,41 @@ The tables below describe the configuration options that are currently supported - The format of the recipe - ``docker`` or ``singularity`` - Yes - * - ``base:image`` - - Base image for ``final`` stage + * - ``images:os`` + - Operating system used as a base for the image - See :ref:`containers-supported-os` - - Yes - * - ``base:spack`` - - Version of Spack + - Yes, if using constrained selection of base images + * - ``images:spack`` + - Version of Spack use in the ``build`` stage - Valid tags for ``base:image`` - - Yes + - Yes, if using constrained selection of base images + * - ``images:build`` + - Image to be used in the ``build`` stage + - Any valid container image + - Yes, if using custom selection of base images + * - ``images:final`` + - Image to be used in the ``build`` stage + - Any valid container image + - Yes, if using custom selection of base images * - ``strip`` - Whether to strip binaries - ``true`` (default) or ``false`` - No - * - ``os_packages`` - - System packages to be installed - - Valid packages for the ``final`` OS + * - ``os_packages:command`` + - Tool used to manage system packages + - ``apt``, ``yum`` + - Only with custom base images + * - ``os_packages:update`` + - Whether or not to update the list of available packages + - True or False (default: True) + - No + * - ``os_packages:build`` + - System packages needed at build-time + - Valid packages for the current OS + - No + * - ``os_packages:final`` + - System packages needed at run-time + - Valid packages for the current OS - No * - ``extra_instructions:build`` - Extra instructions (e.g. `RUN`, `COPY`, etc.) at the end of the ``build`` stage @@ -245,74 +480,6 @@ The tables below describe the configuration options that are currently supported - Description string - No -Once the Environment is properly configured a recipe for a container -image can be printed to standard output by issuing the following -command from the directory where the ``spack.yaml`` resides: - -.. code-block:: console - - $ spack containerize - -The example ``spack.yaml`` above would produce for instance the -following ``Dockerfile``: - -.. code-block:: docker - - # Build stage with Spack pre-installed and ready to be used - FROM spack/centos7:latest as builder - - # What we want to install and how we want to install it - # is specified in a manifest file (spack.yaml) - RUN mkdir /opt/spack-environment \ - && (echo "spack:" \ - && echo " specs:" \ - && echo " - gromacs+mpi" \ - && echo " - mpich" \ - && echo " concretization: together" \ - && echo " config:" \ - && echo " install_tree: /opt/software" \ - && echo " view: /opt/view") > /opt/spack-environment/spack.yaml - - # Install the software, remove unnecessary deps - RUN cd /opt/spack-environment && spack env activate . && spack install && spack gc -y - - # Strip all the binaries - RUN find -L /opt/view/* -type f -exec readlink -f '{}' \; | \ - xargs file -i | \ - grep 'charset=binary' | \ - grep 'x-executable\|x-archive\|x-sharedlib' | \ - awk -F: '{print $1}' | xargs strip -s - - # Modifications to the environment that are necessary to run - RUN cd /opt/spack-environment && \ - spack env activate --sh -d . >> /etc/profile.d/z10_spack_environment.sh - - - # Bare OS image to run the installed executables - FROM centos:7 - - COPY --from=builder /opt/spack-environment /opt/spack-environment - COPY --from=builder /opt/software /opt/software - COPY --from=builder /opt/view /opt/view - COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh - - RUN yum update -y && yum install -y epel-release && yum update -y \ - && yum install -y libgomp \ - && rm -rf /var/cache/yum && yum clean all - - RUN echo 'export PS1="\[$(tput bold)\]\[$(tput setaf 1)\][gromacs]\[$(tput setaf 2)\]\u\[$(tput sgr0)\]:\w $ "' >> ~/.bashrc - - - LABEL "app"="gromacs" - LABEL "mpi"="mpich" - - ENTRYPOINT ["/bin/bash", "--rcfile", "/etc/profile", "-l"] - -.. note:: - Spack can also produce Singularity definition files to build the image. The - minimum version of Singularity required to build a SIF (Singularity Image Format) - from them is ``3.5.3``. - -------------- Best Practices -------------- diff --git a/lib/spack/spack/container/__init__.py b/lib/spack/spack/container/__init__.py index fc3750355a..8206efef01 100644 --- a/lib/spack/spack/container/__init__.py +++ b/lib/spack/spack/container/__init__.py @@ -38,11 +38,11 @@ def validate(configuration_file): env_dict = spack.environment.config_dict(config) env_dict.setdefault('container', { 'format': 'docker', - 'base': {'image': 'ubuntu:18.04', 'spack': 'develop'} + 'images': {'os': 'ubuntu:18.04', 'spack': 'develop'} }) env_dict['container'].setdefault('format', 'docker') env_dict['container'].setdefault( - 'base', {'image': 'ubuntu:18.04', 'spack': 'develop'} + 'images', {'os': 'ubuntu:18.04', 'spack': 'develop'} ) # Remove attributes that are not needed / allowed in the diff --git a/lib/spack/spack/container/images.json b/lib/spack/spack/container/images.json index efaedc68b8..cb495908c9 100644 --- a/lib/spack/spack/container/images.json +++ b/lib/spack/spack/container/images.json @@ -1,42 +1,45 @@ { - "ubuntu:18.04": { - "update": "apt-get -yqq update && apt-get -yqq upgrade", - "install": "apt-get -yqq install", - "clean": "rm -rf /var/lib/apt/lists/*", - "environment": [], - "build": "spack/ubuntu-bionic", - "build_tags": { - "develop": "latest" + "images": { + "ubuntu:18.04": { + "os_package_manager": "apt", + "build": "spack/ubuntu-bionic", + "build_tags": { + "develop": "latest" + } + }, + "ubuntu:16.04": { + "os_package_manager": "apt", + "build": "spack/ubuntu-xenial", + "build_tags": { + "develop": "latest" + } + }, + "centos:7": { + "os_package_manager": "yum", + "environment": [], + "build": "spack/centos7", + "build_tags": { + "develop": "latest" + } + }, + "centos:6": { + "os_package_manager": "yum", + "build": "spack/centos6", + "build_tags": { + "develop": "latest" + } } }, - "ubuntu:16.04": { - "update": "apt-get -yqq update && apt-get -yqq upgrade", - "install": "apt-get -yqq install", - "clean": "rm -rf /var/lib/apt/lists/*", - "environment": [], - "build": "spack/ubuntu-xenial", - "build_tags": { - "develop": "latest" - } - }, - "centos:7": { - "update": "yum update -y && yum install -y epel-release && yum update -y", - "install": "yum install -y", - "clean": "rm -rf /var/cache/yum && yum clean all", - "environment": [], - "build": "spack/centos7", - "build_tags": { - "develop": "latest" - } - }, - "centos:6": { - "update": "yum update -y && yum install -y epel-release && yum update -y", - "install": "yum install -y", - "clean": "rm -rf /var/cache/yum && yum clean all", - "environment": [], - "build": "spack/centos6", - "build_tags": { - "develop": "latest" + "os_package_managers": { + "apt": { + "update": "apt-get -yqq update && apt-get -yqq upgrade", + "install": "apt-get -yqq install", + "clean": "rm -rf /var/lib/apt/lists/*" + }, + "yum": { + "update": "yum update -y && yum install -y epel-release && yum update -y", + "install": "yum install -y", + "clean": "rm -rf /var/cache/yum && yum clean all" } } } diff --git a/lib/spack/spack/container/images.py b/lib/spack/spack/container/images.py index f702781d32..32fcff2770 100644 --- a/lib/spack/spack/container/images.py +++ b/lib/spack/spack/container/images.py @@ -38,7 +38,7 @@ def build_info(image, spack_version): """ # Don't handle error here, as a wrong image should have been # caught by the JSON schema - image_data = data()[image] + image_data = data()["images"][image] build_image = image_data['build'] # Try to check if we have a tag for this Spack version @@ -55,19 +55,30 @@ def build_info(image, spack_version): return build_image, build_tag -def package_info(image): - """Returns the commands used to update system repositories, install - system packages and clean afterwards. +def os_package_manager_for(image): + """Returns the name of the OS package manager for the image + passed as argument. Args: image (str): image to be used at run-time. Should be of the form <image_name>:<image_tag> e.g. "ubuntu:18.04" Returns: + Name of the package manager, e.g. "apt" or "yum" + """ + name = data()["images"][image]["os_package_manager"] + return name + + +def commands_for(package_manager): + """Returns the commands used to update system repositories, install + system packages and clean afterwards. + + Args: + package_manager (str): package manager to be used + + Returns: A tuple of (update, install, clean) commands. """ - image_data = data()[image] - update = image_data['update'] - install = image_data['install'] - clean = image_data['clean'] - return update, install, clean + info = data()["os_package_managers"][package_manager] + return info['update'], info['install'], info['clean'] diff --git a/lib/spack/spack/container/writers/__init__.py b/lib/spack/spack/container/writers/__init__.py index a1d2fa3102..176dd7a50c 100644 --- a/lib/spack/spack/container/writers/__init__.py +++ b/lib/spack/spack/container/writers/__init__.py @@ -13,7 +13,8 @@ import spack.schema.env import spack.tengine as tengine import spack.util.spack_yaml as syaml -from spack.container.images import build_info, package_info +from spack.container.images import build_info, commands_for +from spack.container.images import os_package_manager_for #: Caches all the writers that are currently supported _writer_factory = {} @@ -63,21 +64,34 @@ class PathContext(tengine.Context): @tengine.context_property def run(self): """Information related to the run image.""" - image = self.container_config['base']['image'] + images_config = self.container_config['images'] + + # Check if we have custom images + image = images_config.get('final', None) + # If not use the base OS image + if image is None: + image = images_config['os'] + Run = collections.namedtuple('Run', ['image']) return Run(image=image) @tengine.context_property def build(self): """Information related to the build image.""" + images_config = self.container_config['images'] + + # Check if we have custom images + image = images_config.get('build', None) - # Map the final image to the correct build image - run_image = self.container_config['base']['image'] - spack_version = self.container_config['base']['spack'] - image, tag = build_info(run_image, spack_version) + # If not select the correct build image based on OS and Spack version + if image is None: + operating_system = images_config['os'] + spack_version = images_config['spack'] + image_name, tag = build_info(operating_system, spack_version) + image = ':'.join([image_name, tag]) - Build = collections.namedtuple('Build', ['image', 'tag']) - return Build(image=image, tag=tag) + Build = collections.namedtuple('Build', ['image']) + return Build(image=image) @tengine.context_property def strip(self): @@ -116,14 +130,50 @@ class PathContext(tengine.Context): return syaml.dump(manifest, default_flow_style=False).strip() @tengine.context_property - def os_packages(self): + def os_packages_final(self): """Additional system packages that are needed at run-time.""" - package_list = self.container_config.get('os_packages', None) + return self._os_packages_for_stage('final') + + @tengine.context_property + def os_packages_build(self): + """Additional system packages that are needed at build-time.""" + return self._os_packages_for_stage('build') + + @tengine.context_property + def os_package_update(self): + """Whether or not to update the OS package manager cache.""" + os_packages = self.container_config.get('os_packages', {}) + return os_packages.get('update', True) + + def _os_packages_for_stage(self, stage): + os_packages = self.container_config.get('os_packages', {}) + package_list = os_packages.get(stage, None) + return self._package_info_from(package_list) + + def _package_info_from(self, package_list): + """Helper method to pack a list of packages with the additional + information required by the template. + + Args: + package_list: list of packages + + Returns: + Enough information to know how to update the cache, install + a list opf packages, and clean in the end. + """ if not package_list: return package_list - image = self.container_config['base']['image'] - update, install, clean = package_info(image) + image_config = self.container_config['images'] + image = image_config.get('build', None) + + if image is None: + os_pkg_manager = os_package_manager_for(image_config['os']) + else: + os_pkg_manager = self.container_config['os_packages']['command'] + + update, install, clean = commands_for(os_pkg_manager) + Packages = collections.namedtuple( 'Packages', ['update', 'install', 'list', 'clean'] ) diff --git a/lib/spack/spack/schema/container.py b/lib/spack/spack/schema/container.py index d534afee08..7e14efd75c 100644 --- a/lib/spack/spack/schema/container.py +++ b/lib/spack/spack/schema/container.py @@ -4,6 +4,42 @@ # SPDX-License-Identifier: (Apache-2.0 OR MIT) """Schema for the 'container' subsection of Spack environments.""" +_stages_from_dockerhub = { + 'type': 'object', + 'additionalProperties': False, + 'properties': { + 'os': { + 'type': 'string', + 'enum': ['ubuntu:18.04', + 'ubuntu:16.04', + 'centos:7', + 'centos:6'] + }, + 'spack': { + 'type': 'string', + }, + }, + 'required': ['os', 'spack'] +} + +_custom_stages = { + 'type': 'object', + 'additionalProperties': False, + 'properties': { + 'build': {'type': 'string'}, + 'final': {'type': 'string'} + }, + 'required': ['build', 'final'] +} + +#: List of packages for the schema below +_list_of_packages = { + 'type': 'array', + 'items': { + 'type': 'string' + } +} + #: Schema for the container attribute included in Spack environments container_schema = { 'type': 'object', @@ -16,23 +52,7 @@ container_schema = { }, # Describes the base image to start from and the version # of Spack to be used - 'base': { - 'type': 'object', - 'additionalProperties': False, - 'properties': { - 'image': { - 'type': 'string', - 'enum': ['ubuntu:18.04', - 'ubuntu:16.04', - 'centos:7', - 'centos:6'] - }, - 'spack': { - 'type': 'string', - }, - }, - 'required': ['image', 'spack'] - }, + 'images': {'anyOf': [_stages_from_dockerhub, _custom_stages]}, # Whether or not to strip installed binaries 'strip': { 'type': 'boolean', @@ -40,10 +60,14 @@ container_schema = { }, # Additional system packages that are needed at runtime 'os_packages': { - 'type': 'array', - 'items': { - 'type': 'string' - } + 'type': 'object', + 'properties': { + 'command': {'type': 'string', 'enum': ['apt', 'yum']}, + 'update': {'type': 'boolean'}, + 'build': _list_of_packages, + 'final': _list_of_packages + }, + 'additionalProperties': False }, # Add labels to the image 'labels': { diff --git a/lib/spack/spack/tengine.py b/lib/spack/spack/tengine.py index e1e059c6a6..8581c8b41e 100644 --- a/lib/spack/spack/tengine.py +++ b/lib/spack/spack/tengine.py @@ -80,7 +80,9 @@ def make_environment(dirs=None): # Loader for the templates loader = jinja2.FileSystemLoader(dirs) # Environment of the template engine - env = jinja2.Environment(loader=loader, trim_blocks=True) + env = jinja2.Environment( + loader=loader, trim_blocks=True, lstrip_blocks=True + ) # Custom filters _set_filters(env) return env diff --git a/lib/spack/spack/test/container/conftest.py b/lib/spack/spack/test/container/conftest.py index 65a89b0748..70a1ae2547 100644 --- a/lib/spack/spack/test/container/conftest.py +++ b/lib/spack/spack/test/container/conftest.py @@ -18,8 +18,8 @@ def minimal_configuration(): ], 'container': { 'format': 'docker', - 'base': { - 'image': 'ubuntu:18.04', + 'images': { + 'os': 'ubuntu:18.04', 'spack': 'develop' } } diff --git a/lib/spack/spack/test/container/docker.py b/lib/spack/spack/test/container/docker.py index f75ae7de2a..6392f97db2 100644 --- a/lib/spack/spack/test/container/docker.py +++ b/lib/spack/spack/test/container/docker.py @@ -22,20 +22,22 @@ def test_build_and_run_images(minimal_configuration): # Test the output of the build property build = writer.build - assert build.image == 'spack/ubuntu-bionic' - assert build.tag == 'latest' + assert build.image == 'spack/ubuntu-bionic:latest' def test_packages(minimal_configuration): # In this minimal configuration we don't have packages writer = writers.create(minimal_configuration) - assert writer.os_packages is None + assert writer.os_packages_build is None + assert writer.os_packages_final is None # If we add them a list should be returned pkgs = ['libgomp1'] - minimal_configuration['spack']['container']['os_packages'] = pkgs + minimal_configuration['spack']['container']['os_packages'] = { + 'final': pkgs + } writer = writers.create(minimal_configuration) - p = writer.os_packages + p = writer.os_packages_final assert p.update assert p.install assert p.clean diff --git a/lib/spack/spack/test/container/images.py b/lib/spack/spack/test/container/images.py index 6af69e0c83..6cec1cc592 100644 --- a/lib/spack/spack/test/container/images.py +++ b/lib/spack/spack/test/container/images.py @@ -22,7 +22,8 @@ def test_build_info(image, spack_version, expected): 'ubuntu:18.04' ]) def test_package_info(image): - update, install, clean = spack.container.images.package_info(image) + pkg_manager = spack.container.images.os_package_manager_for(image) + update, install, clean = spack.container.images.commands_for(pkg_manager) assert update assert install assert clean diff --git a/lib/spack/spack/test/container/schema.py b/lib/spack/spack/test/container/schema.py index 3f33a3f9f7..e86f9d3fe8 100644 --- a/lib/spack/spack/test/container/schema.py +++ b/lib/spack/spack/test/container/schema.py @@ -10,7 +10,7 @@ import spack.schema.container def test_images_in_schema(): properties = spack.schema.container.container_schema['properties'] allowed_images = set( - properties['base']['properties']['image']['enum'] + properties['images']['anyOf'][0]['properties']['os']['enum'] ) - images_in_json = set(x for x in spack.container.images.data()) + images_in_json = set(x for x in spack.container.images.data()['images']) assert images_in_json == allowed_images diff --git a/share/spack/templates/container/Dockerfile b/share/spack/templates/container/Dockerfile index c65fce5627..3623a7ba0b 100644 --- a/share/spack/templates/container/Dockerfile +++ b/share/spack/templates/container/Dockerfile @@ -1,12 +1,19 @@ # Build stage with Spack pre-installed and ready to be used -FROM {{ build.image }}:{{ build.tag }} as builder +FROM {{ build.image }} as builder + +{% if os_packages_build %} +# Install OS packages needed to build the software +RUN {% if os_package_update %}{{ os_packages_build.update }} \ + && {% endif %}{{ os_packages_build.install }} {{ os_packages_build.list | join | replace('\n', ' ') }} \ + && {{ os_packages_build.clean }} +{% endif %} # What we want to install and how we want to install it # is specified in a manifest file (spack.yaml) RUN mkdir {{ paths.environment }} \ {{ manifest }} > {{ paths.environment }}/spack.yaml -# Install the software, remove unecessary deps +# Install the software, remove unnecessary deps RUN cd {{ paths.environment }} && spack env activate . && spack install --fail-fast && spack gc -y {% if strip %} @@ -34,16 +41,15 @@ COPY --from=builder {{ paths.store }} {{ paths.store }} COPY --from=builder {{ paths.view }} {{ paths.view }} COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh -{% if os_packages %} -RUN {{ os_packages.update }} \ - && {{ os_packages.install }}{% for pkg in os_packages.list %} {{ pkg }}{% endfor %} \ - && {{ os_packages.clean }} +{% if os_packages_final %} +RUN {% if os_package_update %}{{ os_packages_final.update }} \ + && {% endif %}{{ os_packages_final.install }} {{ os_packages_final.list | join | replace('\n', ' ') }} \ + && {{ os_packages_final.clean }} {% endif %} {% if extra_instructions.final %} {{ extra_instructions.final }} {% endif %} - {% for label, value in labels.items() %} LABEL "{{ label }}"="{{ value }}" {% endfor %} diff --git a/share/spack/templates/container/singularity.def b/share/spack/templates/container/singularity.def index 44b22aeae6..33d775b024 100644 --- a/share/spack/templates/container/singularity.def +++ b/share/spack/templates/container/singularity.def @@ -1,8 +1,17 @@ Bootstrap: docker -From: {{ build.image }}:{{ build.tag }} +From: {{ build.image }} Stage: build %post +{% if os_packages_build.list %} + # Update, install and cleanup of system packages needed at build-time + {% if os_package_update %} + {{ os_packages_build.update }} + {% endif %} + {{ os_packages_build.install }} {{ os_packages_build.list | join | replace('\n', ' ') }} + {{ os_packages_build.clean }} + +{% endif %} # Create the manifest file for the installation in /opt/spack-environment mkdir {{ paths.environment }} && cd {{ paths.environment }} cat << EOF > spack.yaml @@ -29,7 +38,6 @@ EOF {{ extra_instructions.build }} {% endif %} - {% if apps %} {% for application, help_text in apps.items() %} @@ -52,39 +60,41 @@ Stage: final {{ paths.environment }}/environment_modifications.sh {{ paths.environment }}/environment_modifications.sh %post -{% if os_packages.list %} - # Update, install and cleanup of system packages - {{ os_packages.update }} - {{ os_packages.install }} {{ os_packages.list | join | replace('\n', ' ') }} - {{ os_packages.clean }} +{% if os_packages_final.list %} + # Update, install and cleanup of system packages needed at run-time + {% if os_package_update %} + {{ os_packages_final.update }} + {% endif %} + {{ os_packages_final.install }} {{ os_packages_final.list | join | replace('\n', ' ') }} + {{ os_packages_final.clean }} {% endif %} # Modify the environment without relying on sourcing shell specific files at startup cat {{ paths.environment }}/environment_modifications.sh >> $SINGULARITY_ENVIRONMENT {% if extra_instructions.final %} {{ extra_instructions.final }} {% endif %} - {% if runscript %} + %runscript {{ runscript }} {% endif %} - {% if startscript %} + %startscript {{ startscript }} {% endif %} - {% if test %} + %test {{ test }} {% endif %} - {% if help %} + %help {{ help }} {% endif %} - {% if labels %} + %labels {% for label, value in labels.items() %} {{ label }} {{ value }} |