diff --git a/CHANGES/424.feature b/CHANGES/424.feature new file mode 100644 index 000000000..e960304b0 --- /dev/null +++ b/CHANGES/424.feature @@ -0,0 +1 @@ +Added container build/remove image commands. diff --git a/pulpcore/cli/container/context.py b/pulpcore/cli/container/context.py index f90111ce6..15bec518f 100644 --- a/pulpcore/cli/container/context.py +++ b/pulpcore/cli/container/context.py @@ -108,6 +108,7 @@ class PulpContainerRepositoryContext(PulpContainerBaseRepositoryContext): "pulpexport": [PluginRequirement("container", "2.8.0")], "tag": [PluginRequirement("container", "2.3.0")], "roles": [PluginRequirement("container", "2.11.0")], + "build": [PluginRequirement("container", "1.1.0")], } def modify( @@ -143,6 +144,16 @@ def copy_manifest( body = self.preprocess_body(body) 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[str], + ) -> Any: + body = {"containerfile_artifact": container_artifact, "tag": tag, "artifacts": 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" @@ -153,6 +164,10 @@ class PulpContainerPushRepositoryContext(PulpContainerBaseRepositoryContext): "roles": [PluginRequirement("container", "2.11.0")], } + def remove_image(self, digest: str) -> Any: + body = {"digest": digest} + return self.call("remove_image", parameters={self.HREF: self.pulp_href}, body=body) + registered_repository_contexts["container:container"] = PulpContainerRepositoryContext registered_repository_contexts["container:push"] = PulpContainerPushRepositoryContext diff --git a/pulpcore/cli/container/repository.py b/pulpcore/cli/container/repository.py index 79102d75e..d20527194 100644 --- a/pulpcore/cli/container/repository.py +++ b/pulpcore/cli/container/repository.py @@ -1,13 +1,17 @@ +import json import re -from typing import Any, Dict, List, Optional +from pathlib import Path +from typing import Any, Dict, List, Optional, Union import click from pulpcore.cli.common.context import ( EntityFieldDefinition, + PulpContext, PulpEntityContext, PulpRemoteContext, PulpRepositoryContext, + pass_pulp_context, pass_repository_context, ) from pulpcore.cli.common.generic import ( @@ -17,6 +21,7 @@ label_command, label_select_option, list_command, + load_json_callback, name_option, pulp_group, repository_content_command, @@ -41,6 +46,7 @@ PulpContainerRepositoryContext, PulpContainerTagContext, ) +from pulpcore.cli.core.context import PulpArtifactContext from pulpcore.cli.core.generic import task_command translation = get_translation(__name__) @@ -57,6 +63,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[str, Path, None]: + if not value: + return value + uvalue: Union[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", @@ -114,6 +136,7 @@ def repository() -> None: "blob": PulpContainerBlobContext, } container_context = (PulpContainerRepositoryContext,) +push_container_context = (PulpContainerPushRepositoryContext,) repository.add_command(list_command(decorators=[label_select_option])) repository.add_command(show_command(decorators=lookup_options)) @@ -286,3 +309,91 @@ def copy_manifest( digests=digests or None, media_types=media_types or None, ) + + +def upload_file(pulp_ctx: PulpContext, file_location: str) -> str: + try: + with click.open_file(file_location, "r") 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[str, Path, None], +) -> None: + if not repository_ctx.capable("build"): + raise click.ClickException(_("Repository does not support image building.")) + + container_artifact_href: str + artifacts_json: Optional[str] = None + # 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 artifacts: + if isinstance(artifacts, Path): + # Upload files in directory + artifact_hrefs = {} + for child in artifacts.rglob("*"): + if child.is_file(): + artifact_href = upload_file(pulp_ctx, str(child)) + artifact_hrefs[artifact_href] = str(child.relative_to(artifacts)) + artifacts_json = json.dumps(artifact_hrefs) + else: + artifacts_json = artifacts + + repository_ctx.build_image(container_artifact_href, tag, artifacts_json) + + +@repository.command(allowed_with_contexts=push_container_context) +@name_option +@href_option +@click.option("--digest", help=_("SHA256 digest of the Manifest file"), required=True) +@pass_repository_context +def remove_image( + repository_ctx: PulpContainerPushRepositoryContext, + digest: str, +) -> None: + digest = digest.strip() + if not digest.startswith("sha256:"): + digest = f"sha256:{digest}" + if len(digest) != 71: # len("sha256:") + 64 + raise click.ClickException("Improper SHA256, please provide a valid 64 digit digest.") + + repository_ctx.remove_image(digest)