Docker implementation#1219
Conversation
9fa7db8 to
9f20cbe
Compare
| } | ||
| ], | ||
| "default": null, | ||
| "description": "If set, builds a docker image using the Dockerfile generated by constructor and saves it as a portable tarball either uncompressed or compressed. ``<name>-<version>-<platform>-<arch>-docker.tar`` will be created in the output docker directory.", |
There was a problem hiding this comment.
Is the format <name>-<version>-<platform>-<arch> true?
From what I could see in the code it looks like:
tag = info.get("docker_tag", f"{info['name'].lower()}:{info['version']}")
| The labels `org.opencontainers.image.title` and `org.opencontainers.image.version` | ||
| are set automatically from `name` and `version`. | ||
| """ | ||
| docker_image: Literal["tar", "gz", "zst"] | None = None |
There was a problem hiding this comment.
Should we add explicit NotImplementedError for those values that are not yet implemented? I see in the PR it says:
(gz, zst) are defined in the schema but not yet implemented.
lrandersson
left a comment
There was a problem hiding this comment.
Well done Jaida! In the future I think we should split these large PRs into smaller separate ones (to simplify review) If the comments are addressed from Marco I approve!
marcoesters
left a comment
There was a problem hiding this comment.
Good changes - a few more issues to address.
| The labels `org.opencontainers.image.title` and `org.opencontainers.image.version` | ||
| are set automatically from `name` and `version`. | ||
| """ | ||
| docker_image: Literal["tar", "gz", "zst"] | None = None |
There was a problem hiding this comment.
We shouldn't allow inputs we don't support. It's okay to only allow "tar" for now. Also, should we call it docker_image_format? I feel like that's more descriptive.
There was a problem hiding this comment.
I agree, and we don't allow inputs we don't support and"tar" is the only one allowed because we reject anything outside of tar with a NotImplementedError. gz and zst are left intentionally as placeholders to document the planned formats so the research isn't lost and users understand the direction. The other compressions including bz2 may be used as well, but they would need to be used manually. Eventually, constructor could handle this, but I think it's better suited for a different PR. Is this wording better?
docker_image: Literal["tar", "gz", "zst"] | None = None
"""
If set, builds a docker image using the Dockerfile generated by constructor and saves it as a portable file. Currently, only ``tar``(uncompressed) is supported. Additional formats (``gz``, ``zst``) are planned for future releases.
``<name>-<version>-<platform>-docker.<extension>`` will be created in the output directory.
"""
As far as the docker_image_format, it is more descriptive, but I think docker_image communicates that the user is opting into having the image built, which feels right — the name is the feature toggle, not just a format selector. docker_image_format reads more like a sub-option for something already enabled.
| docker_image: Literal["tar", "gz", "zst"] | None = None | ||
| """ | ||
| If set, builds a docker image using the Dockerfile generated by constructor and saves it as a portable tarball either uncompressed or compressed. | ||
| ``<name>-<version>-<platform>-<arch>-docker.tar`` will be created in the output docker directory. |
There was a problem hiding this comment.
| ``<name>-<version>-<platform>-<arch>-docker.tar`` will be created in the output docker directory. | |
| ``<name>-<version>-<platform>-<arch>-docker.<extension>`` will be created in the output docker directory. |
| name=info["name"], | ||
| version=info["version"], | ||
| labels=info.get("docker_labels", {}), | ||
| init_cmd="$PREFIX/bin/mamba shell" if has_mamba else "$PREFIX/bin/python -m conda", |
There was a problem hiding this comment.
This is still assuming that either mamba or conda must be in the base environment. This is not necessarily true. We need to implement the same has_conda check as for mamba. Also note that mamba v1 uses a different init command than mamba v2.
There was a problem hiding this comment.
Good catch. Moving this to the template and adding a v1 fallback
| use the `default_prefix_all_users` key. If not provided, the default prefix | ||
| is `%USERPROFILE%\\<NAME>`. Environment variables will be expanded at | ||
| install time. | ||
| install time. If creating a Docker output, the default is `/opt/<NAME>` and can be overridden during the Docker build process. |
There was a problem hiding this comment.
This should mention which variable to use to override it.
| "Install Docker Buildx to proceed, or " | ||
| "use `installer_type: docker` in construct.yaml to " | ||
| "generate the Dockerfile without building the image." |
There was a problem hiding this comment.
Presumably, this is already the case. The correct remedy is to remove docker_image.
There was a problem hiding this comment.
This file is nearly identical to dockerfile. You could use Jinja and an environment variable to consolidate the two.
There was a problem hiding this comment.
The overlap is intentional. One solely tests generating the Dockerfile, while the other tests building the docker image without needing the installer_type: docker set. If I use a single file, then it would make the tests a little chaotic and harder to read, but if you have something specific in mind to handle both options, while maintaining clarity in the tests, then I'm all ears.
| yaml = YAML() | ||
| with open(input_path / "construct.yaml") as f: | ||
| config = yaml.load(f) |
There was a problem hiding this comment.
config only appears to be used to read values from construct.yaml here. I think for testing, it's okay to hard-code what we expect to be in the final Dockerfile. That would also allow you to use Jinja to consolidate the test files.
There was a problem hiding this comment.
I'd rather keep reading from construct.yaml. If the example is updated without updating the test, reading from config keeps them in sync automatically.
| if installer.suffix == ".sh": | ||
| installer_stem = installer.stem |
There was a problem hiding this comment.
Just to be clear, this does not mean we have an SH installer in output_path directly, right? The SH installer only exists in output_path / "installer" and nowhere else? Can we test this to ensure that the SH installer isn't duplicated anywhere else?
| assert f"FROM {config['docker_base_image']}" in dockerfile_text | ||
|
|
||
| for key, value in config.get("docker_labels", {}).items(): | ||
| assert f'{key}="{value}"' in dockerfile_text |
There was a problem hiding this comment.
| assert f'{key}="{value}"' in dockerfile_text | |
| assert f'LABEL {key}="{value}"' in dockerfile_text |
Right?
There was a problem hiding this comment.
Yes, this is more precise.
| COPY {{ installer_filename }} /tmp/installer.sh | ||
|
|
||
| RUN sh /tmp/installer.sh -b -p "${PREFIX}" && \ | ||
| rm -f "${PREFIX}/uninstall.sh" && \ |
There was a problem hiding this comment.
| rm -f "${PREFIX}/uninstall.sh" && \ |
uninstall.sh is not produced by constructor. This is an extra file that should just not be included in the first place.
There was a problem hiding this comment.
uninstall.sh is written to the prefix by the installer script when it runs. The rm -f handles it cleanly.
Description
Summary
This PR adds Docker output support to constructor via two new opt-in flows:
Flow 1: Dockerfile generation (
installer_type: docker)Generates a multi-stage
Dockerfilealongside the.shinstaller and stages bothinto a named output directory. No Docker CLI required — the output can be used
directly or customized before building.
Output structure:
Usage:
The generated Dockerfile uses a two-stage build: Stage 1 runs the
.shinstallerin batch mode and cleans up build artifacts (
.afiles,__pycache__, optionallypkgs/). Stage 2 copies the finished environment into a clean final image andruns
conda init.Flow 2: Portable image tarball (
docker_image: tar)Builds a Docker image from the generated Dockerfile using
docker buildxandexports it as a portable
.tarfile viadocker save. Requires the Docker CLIto be installed on the host. The target platform must be Linux.
Output:
Usage:
New schema keys
docker_base_imagedocker_imagetarto build and export a portable image tarball. Can be used standalone withoutinstaller_type: docker.docker_tagname:version.docker_labelstitleandversionare set automatically.Platform support
linux-*for all Docker features.docker_image: taradditionally requires the Docker CLI on the host.Notes
docker_imageis designed to be extendable. Additional output formats(
gz,zst) are defined in the schema but not yet implemented..shinstaller is always built first and reused as the Docker buildcontext, keeping the two outputs consistent.
Changes
New:
constructor/docker_build.py: Handles Docker output by rendering template and optionally building portable imageconstructor/dockerfile_template.tmpl: Template used to generate Dockerfileexamples/dockerfile/construct.yaml: Example forinstaller_type: dockerflowexamples/docker_image/construct.yaml: Example fordocker_image: tarflowUpdated:
constructor/_schema.py: Addsdockertoinstaller_typeand addsdocker_base_image,docker_tag,docker_labelsconstructor/main.py: Addsdockerto installer typestests/test_examples.py: Addstest_dockerfile_generationandtest_docker_image_buildto cover both flowsChecklist - did you ...
newsdirectory (using the template) for the next release's release notes?