diff --git a/CHANGES/424.feature b/CHANGES/424.feature new file mode 100644 index 000000000..416b89111 --- /dev/null +++ b/CHANGES/424.feature @@ -0,0 +1 @@ +Added container build image command. diff --git a/pulp-glue/pulp_glue/common/context.py b/pulp-glue/pulp_glue/common/context.py index a4e8e400a..15c6f8644 100644 --- a/pulp-glue/pulp_glue/common/context.py +++ b/pulp-glue/pulp_glue/common/context.py @@ -168,6 +168,11 @@ def _patch_api_spec(self) -> None: python_remote_serializer["properties"][prop]["items"] = {"type": "string"} patched_python_remote_serializer["properties"][prop]["type"] = "array" patched_python_remote_serializer["properties"][prop]["items"] = {"type": "string"} + if self.has_plugin(PluginRequirement("container", min="1.1.0")): + # TODO Add upper bound when fixed + oci_build_schema = api_spec["components"]["schemas"]["OCIBuildImage"] + oci_artifacts = oci_build_schema["properties"]["artifacts"] + oci_artifacts["type"] = "string" @property def domain_enabled(self) -> bool: diff --git a/pulp-glue/pulp_glue/container/context.py b/pulp-glue/pulp_glue/container/context.py index 589ada0b2..6ad004611 100644 --- a/pulp-glue/pulp_glue/container/context.py +++ b/pulp-glue/pulp_glue/container/context.py @@ -1,4 +1,5 @@ -from typing import Any, List, Optional +import json +from typing import Any, Dict, List, Optional from pulp_glue.common.context import ( EntityDefinition, @@ -128,6 +129,7 @@ class PulpContainerRepositoryContext(PulpContainerBaseRepositoryContext): "pulpexport": [PluginRequirement("container", min="2.8.0")], "tag": [PluginRequirement("container", min="2.3.0")], "roles": [PluginRequirement("container", min="2.11.0")], + "build": [PluginRequirement("container", min="1.1.0")], } def modify( @@ -167,6 +169,19 @@ def copy_manifest( } return self.call("copy_manifests", parameters={self.HREF: self.pulp_href}, body=body) + def build_image( + self, + container_artifact: str, + tag: Optional[str], + artifacts: Optional[Dict[str, str]], + ) -> Any: + body = {"containerfile_artifact": container_artifact, "tag": tag, "artifacts": artifacts} + # TODO: Add plugin version check when schema is fixed + if artifacts: + body["artifacts"] = json.dumps(artifacts) + body = self.preprocess_body(body) + return self.call("build_image", parameters={self.HREF: self.pulp_href}, body=body) + class PulpContainerPushRepositoryContext(PulpContainerBaseRepositoryContext): HREF = "container_container_push_repository_href" diff --git a/pulpcore/cli/container/repository.py b/pulpcore/cli/container/repository.py index 439bf9d13..51f0375c6 100644 --- a/pulpcore/cli/container/repository.py +++ b/pulpcore/cli/container/repository.py @@ -1,8 +1,14 @@ import re -from typing import Any, Dict, List, Optional +from pathlib import Path +from typing import Any, Dict, List, Optional, Union import click -from pulp_glue.common.context import EntityFieldDefinition, PulpRemoteContext, PulpRepositoryContext +from pulp_glue.common.context import ( + EntityFieldDefinition, + PulpContext, + PulpRemoteContext, + PulpRepositoryContext, +) from pulp_glue.common.i18n import get_translation from pulp_glue.container.context import ( PulpContainerBaseRepositoryContext, @@ -13,6 +19,7 @@ PulpContainerRepositoryContext, PulpContainerTagContext, ) +from pulp_glue.core.context import PulpArtifactContext from pulpcore.cli.common.generic import ( create_command, @@ -21,7 +28,9 @@ label_command, label_select_option, list_command, + load_json_callback, name_option, + pass_pulp_context, pass_repository_context, pulp_group, pulp_labels_option, @@ -53,6 +62,22 @@ def _tag_callback(ctx: click.Context, param: click.Parameter, value: str) -> str return value +def _directory_or_json_callback( + ctx: click.Context, param: click.Parameter, value: Optional[str] +) -> Union[Dict[str, str], Path, None]: + if not value: + return None + uvalue: Union[Dict[str, str], Path] + try: + uvalue = load_json_callback(ctx, param, value) + except click.ClickException: + uvalue = Path(value) + if not uvalue.exists() or not uvalue.is_dir(): + raise click.ClickException(_("{} is not a valid directory").format(value)) + + return uvalue + + source_option = resource_option( "--source", default_plugin="container", @@ -291,6 +316,72 @@ def copy_manifest( ) +def upload_file(pulp_ctx: PulpContext, file_location: str) -> str: + try: + with click.open_file(file_location, "rb") as fp: + artifact_ctx = PulpArtifactContext(pulp_ctx) + artifact_href = artifact_ctx.upload(fp) + except OSError: + raise click.ClickException( + _("Failed to load content from {file}").format(file=file_location) + ) + click.echo(_("Uploaded file: {}").format(artifact_href)) + return artifact_href # type: ignore + + +@repository.command(allowed_with_contexts=container_context) +@name_option +@href_option +@click.option( + "--containerfile", + help=_( + "An artifact href of an uploaded Containerfile. Can also be a local Containerfile to be" + " uploaded using @." + ), + required=True, +) +@click.option("--tag", help=_("A tag name for the new image being built.")) +@click.option( + "--artifacts", + help=_( + "Directory of files to be uploaded and used during the build. Or a JSON string where each" + " key is an artifact href and the value is it's relative path (name) inside the " + "/pulp_working_directory of the build container executing the Containerfile." + ), + callback=_directory_or_json_callback, +) +@pass_repository_context +@pass_pulp_context +def build_image( + pulp_ctx: PulpContext, + repository_ctx: PulpContainerRepositoryContext, + containerfile: str, + tag: Optional[str], + artifacts: Union[Dict[str, str], Path, None], +) -> None: + if not repository_ctx.capable("build"): + raise click.ClickException(_("Repository does not support image building.")) + + container_artifact_href: str + # Upload necessary files as artifacts if specified + if containerfile[0] == "@": + container_artifact_href = upload_file(pulp_ctx, containerfile[1:]) + else: + artifact_ctx = PulpArtifactContext(pulp_ctx, pulp_href=containerfile) + container_artifact_href = artifact_ctx.pulp_href + + if isinstance(artifacts, Path): + artifacts_path = artifacts + # Upload files in directory + artifacts = {} + for child in artifacts_path.rglob("*"): + if child.is_file(): + artifact_href = upload_file(pulp_ctx, str(child)) + artifacts[artifact_href] = str(child.relative_to(artifacts_path)) + + repository_ctx.build_image(container_artifact_href, tag, artifacts) + + @repository.command(allowed_with_contexts=push_container_context) @name_option @href_option diff --git a/tests/assets/test_containerfile b/tests/assets/test_containerfile new file mode 100644 index 000000000..97b9e13c9 --- /dev/null +++ b/tests/assets/test_containerfile @@ -0,0 +1,5 @@ +FROM busybox:latest +# Copy a file using COPY statement. Use the relative path specified in the 'artifacts' parameter. +COPY foo/bar/example.txt /tmp/inside-image.txt +# Print the content of the file when the container starts +CMD ["cat", "/tmp/inside-image.txt"] \ No newline at end of file