Improve Dockerfile recipe generation (#35187)

- Update default image to Ubuntu 22.04 (previously was still Ubuntu 18.04)
- Optionally use depfiles to install the environment within the container
- Allow extending Dockerfile Jinja2 template
- Allow extending Singularity definition file Jinja2 template
- Deprecate previous options to add extra instructions
This commit is contained in:
Massimiliano Culpo 2023-04-03 21:05:19 +02:00 committed by GitHub
parent 3d149a7db2
commit f91968cf6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 15 deletions

View file

@ -444,6 +444,120 @@ attribute:
The minimum version of Singularity required to build a SIF (Singularity Image Format) The minimum version of Singularity required to build a SIF (Singularity Image Format)
image from the recipes generated by Spack is ``3.5.3``. image from the recipes generated by Spack is ``3.5.3``.
------------------------------
Extending the Jinja2 Templates
------------------------------
The Dockerfile and the Singularity definition file that Spack can generate are based on
a few Jinja2 templates that are rendered according to the environment being containerized.
Even though Spack allows a great deal of customization by just setting appropriate values for
the configuration options, sometimes that is not enough.
In those cases, a user can directly extend the template that Spack uses to render the image
to e.g. set additional environment variables or perform specific operations either before or
after a given stage of the build. Let's consider as an example the following structure:
.. code-block:: console
$ tree /opt/environment
/opt/environment
├── data
│ └── data.csv
├── spack.yaml
├── data
└── templates
└── container
└── CustomDockerfile
containing both the custom template extension and the environment manifest file. To use a custom
template, the environment must register the directory containing it, and declare its use under the
``container`` configuration:
.. code-block:: yaml
:emphasize-lines: 7-8,12
spack:
specs:
- hdf5~mpi
concretizer:
unify: true
config:
template_dirs:
- /opt/environment/templates
container:
format: docker
depfile: true
template: container/CustomDockerfile
The template extension can override two blocks, named ``build_stage`` and ``final_stage``, similarly to
the example below:
.. code-block::
:emphasize-lines: 3,8
{% extends "container/Dockerfile" %}
{% block build_stage %}
RUN echo "Start building"
{{ super() }}
{% endblock %}
{% block final_stage %}
{{ super() }}
COPY data /share/myapp/data
{% endblock %}
The recipe that gets generated contains the two extra instruction that we added in our template extension:
.. code-block:: Dockerfile
:emphasize-lines: 4,43
# Build stage with Spack pre-installed and ready to be used
FROM spack/ubuntu-jammy:latest as builder
RUN echo "Start building"
# 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 " - hdf5~mpi" \
&& echo " concretizer:" \
&& echo " unify: true" \
&& echo " config:" \
&& echo " template_dirs:" \
&& echo " - /tmp/environment/templates" \
&& 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 concretize && spack env depfile -o Makefile && make -j $(nproc) && 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 ubuntu:22.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 /opt/view /opt/view
COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh
COPY data /share/myapp/data
ENTRYPOINT ["/bin/bash", "--rcfile", "/etc/profile", "-l", "-c", "$*", "--" ]
CMD [ "/bin/bash" ]
.. _container_config_options: .. _container_config_options:
----------------------- -----------------------
@ -464,6 +578,10 @@ to customize the generation of container recipes:
- The format of the recipe - The format of the recipe
- ``docker`` or ``singularity`` - ``docker`` or ``singularity``
- Yes - Yes
* - ``depfile``
- Whether to use a depfile for installation, or not
- True or False (default)
- No
* - ``images:os`` * - ``images:os``
- Operating system used as a base for the image - Operating system used as a base for the image
- See :ref:`containers-supported-os` - See :ref:`containers-supported-os`
@ -512,14 +630,6 @@ to customize the generation of container recipes:
- System packages needed at run-time - System packages needed at run-time
- Valid packages for the current OS - Valid packages for the current OS
- No - No
* - ``extra_instructions:build``
- Extra instructions (e.g. `RUN`, `COPY`, etc.) at the end of the ``build`` stage
- Anything understood by the current ``format``
- No
* - ``extra_instructions:final``
- Extra instructions (e.g. `RUN`, `COPY`, etc.) at the end of the ``final`` stage
- Anything understood by the current ``format``
- No
* - ``labels`` * - ``labels``
- Labels to tag the image - Labels to tag the image
- Pairs of key-value strings - Pairs of key-value strings

View file

@ -39,10 +39,10 @@ def validate(configuration_file):
# Ensure we have a "container" attribute with sensible defaults set # Ensure we have a "container" attribute with sensible defaults set
env_dict = ev.config_dict(config) env_dict = ev.config_dict(config)
env_dict.setdefault( env_dict.setdefault(
"container", {"format": "docker", "images": {"os": "ubuntu:18.04", "spack": "develop"}} "container", {"format": "docker", "images": {"os": "ubuntu:22.04", "spack": "develop"}}
) )
env_dict["container"].setdefault("format", "docker") env_dict["container"].setdefault("format", "docker")
env_dict["container"].setdefault("images", {"os": "ubuntu:18.04", "spack": "develop"}) env_dict["container"].setdefault("images", {"os": "ubuntu:22.04", "spack": "develop"})
# Remove attributes that are not needed / allowed in the # Remove attributes that are not needed / allowed in the
# container recipe # container recipe

View file

@ -7,6 +7,7 @@
""" """
import collections import collections
import copy import copy
from typing import Optional
import spack.environment as ev import spack.environment as ev
import spack.schema.env import spack.schema.env
@ -131,6 +132,9 @@ class PathContext(tengine.Context):
directly via PATH. directly via PATH.
""" """
# Must be set by derived classes
template_name: Optional[str] = None
def __init__(self, config, last_phase): def __init__(self, config, last_phase):
self.config = ev.config_dict(config) self.config = ev.config_dict(config)
self.container_config = self.config["container"] self.container_config = self.config["container"]
@ -146,6 +150,10 @@ def __init__(self, config, last_phase):
# Record the last phase # Record the last phase
self.last_phase = last_phase self.last_phase = last_phase
@tengine.context_property
def depfile(self):
return self.container_config.get("depfile", False)
@tengine.context_property @tengine.context_property
def run(self): def run(self):
"""Information related to the run image.""" """Information related to the run image."""
@ -280,7 +288,8 @@ def render_phase(self):
def __call__(self): def __call__(self):
"""Returns the recipe as a string""" """Returns the recipe as a string"""
env = tengine.make_environment() env = tengine.make_environment()
t = env.get_template(self.template_name) template_name = self.container_config.get("template", self.template_name)
t = env.get_template(template_name)
return t.render(**self.to_dict()) return t.render(**self.to_dict())

View file

@ -63,6 +63,8 @@
}, },
# Add labels to the image # Add labels to the image
"labels": {"type": "object"}, "labels": {"type": "object"},
# Use a custom template to render the recipe
"template": {"type": "string", "default": None},
# Add a custom extra section at the bottom of a stage # Add a custom extra section at the bottom of a stage
"extra_instructions": { "extra_instructions": {
"type": "object", "type": "object",
@ -82,6 +84,16 @@
}, },
}, },
"docker": {"type": "object", "additionalProperties": False, "default": {}}, "docker": {"type": "object", "additionalProperties": False, "default": {}},
"depfile": {"type": "boolean", "default": False},
},
"deprecatedProperties": {
"properties": ["extra_instructions"],
"message": (
"container:extra_instructions has been deprecated and will be removed "
"in Spack v0.21. Set container:template appropriately to use custom Jinja2 "
"templates instead."
),
"error": False,
}, },
} }

View file

@ -6,6 +6,7 @@
# Build stage with Spack pre-installed and ready to be used # Build stage with Spack pre-installed and ready to be used
FROM {{ build.image }} as builder FROM {{ build.image }} as builder
{% block build_stage %}
{% if os_packages_build %} {% if os_packages_build %}
# Install OS packages needed to build the software # Install OS packages needed to build the software
RUN {% if os_package_update %}{{ os_packages_build.update }} \ RUN {% if os_package_update %}{{ os_packages_build.update }} \
@ -19,7 +20,11 @@ RUN mkdir {{ paths.environment }} \
{{ manifest }} > {{ paths.environment }}/spack.yaml {{ manifest }} > {{ paths.environment }}/spack.yaml
# Install the software, remove unnecessary deps # Install the software, remove unnecessary deps
{% if depfile %}
RUN cd {{ paths.environment }} && spack env activate . && spack concretize && spack env depfile -o Makefile && make -j $(nproc) && spack gc -y
{% else %}
RUN cd {{ paths.environment }} && spack env activate . && spack install --fail-fast && spack gc -y RUN cd {{ paths.environment }} && spack env activate . && spack install --fail-fast && spack gc -y
{% endif %}
{% if strip %} {% if strip %}
# Strip all the binaries # Strip all the binaries
@ -37,7 +42,9 @@ RUN cd {{ paths.environment }} && \
{% if extra_instructions.build %} {% if extra_instructions.build %}
{{ extra_instructions.build }} {{ extra_instructions.build }}
{% endif %} {% endif %}
{% endblock build_stage %}
{% endif %} {% endif %}
{% if render_phase.final %} {% if render_phase.final %}
# Bare OS image to run the installed executables # Bare OS image to run the installed executables
FROM {{ run.image }} FROM {{ run.image }}
@ -48,6 +55,8 @@ COPY --from=builder {{ paths.hidden_view }} {{ paths.hidden_view }}
COPY --from=builder {{ paths.view }} {{ paths.view }} COPY --from=builder {{ paths.view }} {{ paths.view }}
COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh COPY --from=builder /etc/profile.d/z10_spack_environment.sh /etc/profile.d/z10_spack_environment.sh
{% block final_stage %}
{% if os_packages_final %} {% if os_packages_final %}
RUN {% if os_package_update %}{{ os_packages_final.update }} \ RUN {% if os_package_update %}{{ os_packages_final.update }} \
&& {% endif %}{{ os_packages_final.install }} {{ os_packages_final.list | join | replace('\n', ' ') }} \ && {% endif %}{{ os_packages_final.install }} {{ os_packages_final.list | join | replace('\n', ' ') }} \
@ -57,6 +66,7 @@ RUN {% if os_package_update %}{{ os_packages_final.update }} \
{{ extra_instructions.final }} {{ extra_instructions.final }}
{% endif %} {% endif %}
{% endblock final_stage %}
{% for label, value in labels.items() %} {% for label, value in labels.items() %}
LABEL "{{ label }}"="{{ value }}" LABEL "{{ label }}"="{{ value }}"
{% endfor %} {% endfor %}

View file

@ -3,6 +3,7 @@ From: {{ build.image }}
Stage: build Stage: build
%post %post
{% block build_stage %}
{% if os_packages_build.list %} {% if os_packages_build.list %}
# Update, install and cleanup of system packages needed at build-time # Update, install and cleanup of system packages needed at build-time
{% if os_package_update %} {% if os_package_update %}
@ -20,10 +21,14 @@ EOF
# Install all the required software # Install all the required software
. /opt/spack/share/spack/setup-env.sh . /opt/spack/share/spack/setup-env.sh
spack env activate . spack -e . concretize
spack install --fail-fast {% if depfile %}
spack -e . env depfile -o Makefile
make -j $(nproc)
{% else %}
spack -e . install
{% endif %}
spack gc -y spack gc -y
spack env deactivate
spack env activate --sh -d . >> {{ paths.environment }}/environment_modifications.sh spack env activate --sh -d . >> {{ paths.environment }}/environment_modifications.sh
{% if strip %} {% if strip %}
@ -37,7 +42,7 @@ EOF
{% if extra_instructions.build %} {% if extra_instructions.build %}
{{ extra_instructions.build }} {{ extra_instructions.build }}
{% endif %} {% endif %}
{% endblock build_stage %}
{% if apps %} {% if apps %}
{% for application, help_text in apps.items() %} {% for application, help_text in apps.items() %}
@ -61,6 +66,7 @@ Stage: final
{{ paths.environment }}/environment_modifications.sh {{ paths.environment }}/environment_modifications.sh {{ paths.environment }}/environment_modifications.sh {{ paths.environment }}/environment_modifications.sh
%post %post
{% block final_stage %}
{% if os_packages_final.list %} {% if os_packages_final.list %}
# Update, install and cleanup of system packages needed at run-time # Update, install and cleanup of system packages needed at run-time
{% if os_package_update %} {% if os_package_update %}
@ -74,6 +80,7 @@ Stage: final
{% if extra_instructions.final %} {% if extra_instructions.final %}
{{ extra_instructions.final }} {{ extra_instructions.final }}
{% endif %} {% endif %}
{% endblock final_stage %}
{% if runscript %} {% if runscript %}
%runscript %runscript