diff --git a/CHANGES/479.feature b/CHANGES/479.feature new file mode 100644 index 000000000..f7d132828 --- /dev/null +++ b/CHANGES/479.feature @@ -0,0 +1,5 @@ +Updated the image building feature: + * Replaced `artifacts` by `build_context` (i.e., a file plugin repository version HREF) + as the parameter to provide the build context for a Containerfile. + * Added the `containerfile_name` as the parameter to provide the relative-path of a File Content + from the provided `build_context`. diff --git a/CHANGES/479.removal b/CHANGES/479.removal new file mode 100644 index 000000000..6bc919630 --- /dev/null +++ b/CHANGES/479.removal @@ -0,0 +1,2 @@ +Support for utilizing raw artifacts, as the build context and for the Containerfile, +during image builds has been removed. diff --git a/docs/admin/guides/build-image.md b/docs/admin/guides/build-image.md index b71f69e9c..b5f5b70ba 100644 --- a/docs/admin/guides/build-image.md +++ b/docs/admin/guides/build-image.md @@ -6,24 +6,33 @@ guaranteed. Users can add new images to a container repository by uploading a Containerfile. The syntax for -Containerfile is the same as for a Dockerfile. The same REST API endpoint also accepts a JSON -string that maps artifacts in Pulp to a filename. Any artifacts passed in are available inside the -build container at `/pulp_working_directory`. +Containerfile is the same as for a Dockerfile. -## Create a Repository +To pass arbitrary files or artifacts to the image building context, the `build_context` property (a reference to a file repository) can be provided in the request payload. +These files can be referenced in Containerfile by passing their `relative-path`: +``` +ADD/COPY +``` + +It is possible to define the Containerfile in two ways: +* from a [local file](site:pulp_container/docs/admin/guides/build-image#build-from-a-containerfile-uploaded-during-build-request) and pass it during build request +* from an [existing file](site:pulp_container/docs/admin/guides/build-image#upload-the-containerfile-as-a-file-content) in the `build_context` + +## Create a Container Repository ```bash -REPO_HREF=$(pulp container repository create --name building | jq -r '.pulp_href') +CONTAINER_REPO=$(pulp container repository create --name building | jq -r '.pulp_href') ``` -## Create an Artifact +## Create a File Repository and populate it ```bash +FILE_REPO=$(pulp file repository create --name bar --autopublish | jq -r '.pulp_href') + echo 'Hello world!' > example.txt -ARTIFACT_HREF=$(http --form POST http://localhost/pulp/api/v3/artifacts/ \ - file@./example.txt \ - | jq -r '.pulp_href') +pulp file content upload --relative-path foo/bar/example.txt \ +--file ./example.txt --repository bar ``` ## Create a Containerfile @@ -31,22 +40,38 @@ ARTIFACT_HREF=$(http --form POST http://localhost/pulp/api/v3/artifacts/ \ ```bash echo 'FROM centos:7 -# Copy a file using COPY statement. Use the relative path specified in the 'artifacts' parameter. +# Copy a file using COPY statement. Use the path specified in the '--relative-path' parameter. COPY foo/bar/example.txt /inside-image.txt # Print the content of the file when the container starts CMD ["cat", "/inside-image.txt"]' >> Containerfile ``` -## Build an OCI image + +## Build from a Containerfile uploaded during build request + +```bash +TASK_HREF=$(http --form POST ${BASE_ADDR}${CONTAINER_REPO}'build_image/' "containerfile@./Containerfile" \ +build_context=${FILE_REPO}versions/1/ | jq -r '.task') +``` + +## Upload the Containerfile to a File Repository and use it to build + +### Upload the Containerfile as a File Content + +```bash +pulp file content upload --relative-path MyContainerfile --file ./Containerfile --repository bar +``` + +### Build an OCI image from a Containerfile present in build_context ```bash -TASK_HREF=$(http --form POST :$REPO_HREF'build_image/' containerfile@./Containerfile \ -artifacts="{\"$ARTIFACT_HREF\": \"foo/bar/example.txt\"}" | jq -r '.task') +TASK_HREF=$(http --form POST ${BASE_ADDR}${CONTAINER_REPO}'build_image/' containerfile_name=MyContainerfile \ +build_context=${FILE_REPO}versions/2/ | jq -r '.task') ``` + !!! warning - Non-staff users, lacking read access to the `artifacts` endpoint, may encounter restricted - functionality as they are prohibited from listing artifacts uploaded to Pulp and utilizing - them within the build process. + File repositories synced with the on-demand policy will not automatically download the missing artifacts. + Trying to build an image using a file that has not yet been downloaded will fail. diff --git a/pulp_container/app/serializers.py b/pulp_container/app/serializers.py index d2922bf18..1287e49fe 100644 --- a/pulp_container/app/serializers.py +++ b/pulp_container/app/serializers.py @@ -1,12 +1,10 @@ from gettext import gettext as _ -import os import re from django.core.validators import URLValidator from rest_framework import serializers from pulpcore.plugin.models import ( - Artifact, ContentRedirectContentGuard, Remote, Repository, @@ -30,9 +28,11 @@ ValidateFieldsMixin, ) +from pulp_file.app.models import FileContent from pulp_container.app import models from pulp_container.constants import SIGNATURE_TYPE + VALID_SIGNATURE_NAME_REGEX = r"^sha256:[0-9a-f]{64}@[0-9a-f]{32}$" VALID_TAG_REGEX = r"^[A-Za-z0-9][A-Za-z0-9._-]*$" VALID_BASE_PATH_REGEX_COMPILED = re.compile(r"^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9])*$") @@ -758,13 +758,12 @@ class OCIBuildImageSerializer(ValidateFieldsMixin, serializers.Serializer): A repository must be specified, to which the container image content will be added. """ - containerfile_artifact = RelatedField( - many=False, - lookup_field="pk", - view_name="artifacts-detail", - queryset=Artifact.objects.all(), + containerfile_name = serializers.CharField( + required=False, + allow_blank=True, help_text=_( - "Artifact representing the Containerfile that should be used to run podman-build." + "Name of the Containerfile, from build_context, that should be used to run " + "podman-build." ), ) containerfile = serializers.FileField( @@ -774,65 +773,77 @@ class OCIBuildImageSerializer(ValidateFieldsMixin, serializers.Serializer): tag = serializers.CharField( required=False, default="latest", help_text="A tag name for the new image being built." ) - artifacts = serializers.JSONField( + build_context = RepositoryVersionRelatedField( required=False, - help_text="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.", + help_text=_("RepositoryVersion to be used as the build context for container images."), + allow_null=True, + queryset=RepositoryVersion.objects.filter(repository__pulp_type="file.file"), ) - def __init__(self, *args, **kwargs): - """Initializer for OCIBuildImageSerializer.""" - super().__init__(*args, **kwargs) - self.fields["containerfile_artifact"].required = False - def validate(self, data): """Validates that all the fields make sense.""" data = super().validate(data) - if "containerfile" in data: - if "containerfile_artifact" in data: - raise serializers.ValidationError( - _("Only one of 'containerfile' and 'containerfile_artifact' may be specified.") - ) - data["containerfile_artifact"] = Artifact.init_and_validate(data.pop("containerfile")) - elif "containerfile_artifact" in data: - data["containerfile_artifact"].touch() - else: + if bool(data.get("containerfile", None)) == bool(data.get("containerfile_name", None)): + raise serializers.ValidationError( + _("Exactly one of 'containerfile' or 'containerfile_name' must be specified.") + ) + + if "containerfile_name" in data and "build_context" not in data: raise serializers.ValidationError( - _("'containerfile' or 'containerfile_artifact' must " "be specified.") + _("A 'build_context' must be specified when 'containerfile_name' is present.") ) - artifacts = {} - if "artifacts" in data: - for url, relative_path in data["artifacts"].items(): - if os.path.isabs(relative_path): + + # TODO: this can be removed after https://github.com/pulp/pulpcore/issues/5786 + if data.get("build_context", None): + data["repository_version"] = data["build_context"] + + return data + + def deferred_files_validation(self, data): + """ + Defer the validation of on_demand_artifacts and the `Containerfile` to avoid rerunning + unnecessary database queries when checking permissions (DRF Access Policy). + """ + if build_context := data.get("build_context", None): + + # check if the on_demand_artifacts exist + for on_demand_artifact in build_context.on_demand_artifacts.iterator(): + if not on_demand_artifact.content_artifact.artifact: raise serializers.ValidationError( - _("Relative path cannot start with '/'. " "{0}").format(relative_path) + _( + "It is not possible to use File content synced with on-demand " + "policy without pulling the data first." + ) + ) + + # check if the containerfile_name exists in the build_context (File Repository) + if ( + data.get("containerfile_name", None) + and not FileContent.objects.filter( + repositories__in=[build_context.repository.pk], + relative_path=data["containerfile_name"], + ).exists() + ): + raise serializers.ValidationError( + _( + 'Could not find the Containerfile "' + + data["containerfile_name"] + + '" in the build_context provided' ) - artifactfield = RelatedField( - view_name="artifacts-detail", - queryset=Artifact.objects.all(), - source="*", - initial=url, ) - try: - artifact = artifactfield.run_validation(data=url) - artifact.touch() - artifacts[str(artifact.pk)] = relative_path - except serializers.ValidationError as e: - # Append the URL of missing Artifact to the error message - e.detail[0] = "%s %s" % (e.detail[0], url) - raise e - data["artifacts"] = artifacts + + data["build_context_pk"] = build_context.repository.pk + return data class Meta: fields = ( - "containerfile_artifact", + "containerfile_name", "containerfile", "repository", "tag", - "artifacts", + "build_context", ) diff --git a/pulp_container/app/tasks/__init__.py b/pulp_container/app/tasks/__init__.py index 09f335a3d..af37e688a 100644 --- a/pulp_container/app/tasks/__init__.py +++ b/pulp_container/app/tasks/__init__.py @@ -1,5 +1,5 @@ from .download_image_data import download_image_data # noqa -from .builder import build_image_from_containerfile # noqa +from .builder import build_image_from_containerfile, build_image # noqa from .recursive_add import recursive_add_content # noqa from .recursive_remove import recursive_remove_content # noqa from .sign import sign # noqa diff --git a/pulp_container/app/tasks/builder.py b/pulp_container/app/tasks/builder.py index 58daa3796..80d87b339 100644 --- a/pulp_container/app/tasks/builder.py +++ b/pulp_container/app/tasks/builder.py @@ -14,7 +14,12 @@ ) from pulp_container.constants import MEDIA_TYPE from pulp_container.app.utils import calculate_digest -from pulpcore.plugin.models import Artifact, ContentArtifact, Content +from pulpcore.plugin.models import ( + Artifact, + ContentArtifact, + Content, + PulpTemporaryFile, +) def get_or_create_blob(layer_json, manifest, path): @@ -43,7 +48,7 @@ def get_or_create_blob(layer_json, manifest, path): artifact=layer_artifact, content=blob, relative_path=layer_json["digest"] ).save() if layer_json["mediaType"] != MEDIA_TYPE.CONFIG_BLOB_OCI: - BlobManifest(manifest=manifest, manifest_blob=blob).save() + BlobManifest.objects.get_or_create(manifest=manifest, manifest_blob=blob) return blob @@ -68,15 +73,13 @@ def add_image_from_directory_to_repository(path, repository, tag): manifest_digest = calculate_digest(bytes_data) manifest_text_data = bytes_data.decode("utf-8") - manifest = Manifest( + manifest, _ = Manifest.objects.get_or_create( digest=manifest_digest, schema_version=2, media_type=MEDIA_TYPE.MANIFEST_OCI, data=manifest_text_data, ) - manifest.save() - tag = Tag(name=tag, tagged_manifest=manifest) - tag.save() + tag, _ = Tag.objects.get_or_create(name=tag, tagged_manifest=manifest) with repository.new_version() as new_repo_version: manifest_json = json.loads(manifest_text_data) @@ -95,10 +98,100 @@ def add_image_from_directory_to_repository(path, repository, tag): return new_repo_version +def build_image( + containerfile_name=None, + containerfile_tempfile_pk=None, + build_context_pk=None, + repository_pk=None, + tag=None, +): + """ + Builds an OCI container image from a Containerfile. + + The artifacts are made available inside the build container at the paths specified by their + values. The Containerfile can make use of these files during build process. + + Args: + containerfile_name (str): The Containerfile relative_path from the build_context repository + containerfile_tempfile_pk (str): The pk of a PulpTemporaryFile that contains + the Containerfile + build_context_pk (str): The pk of the RepositoryVersion used as the build context + repository_pk (str): The pk of a Repository to add the OCI container image + tag (str): Tag name for the new image in the repository + + Returns: + A class:`pulpcore.plugin.models.RepositoryVersion` that contains the new OCI container + image and tag. + + """ + if not containerfile_tempfile_pk and not containerfile_name: + raise RuntimeError("Neither a name nor temporary file for the Containerfile was specified.") + + if containerfile_tempfile_pk: + containerfile_artifact = PulpTemporaryFile.objects.get(pk=containerfile_tempfile_pk) + + repository = ContainerRepository.objects.get(pk=repository_pk) + name = str(uuid4()) + with tempfile.TemporaryDirectory(dir=".") as working_directory: + working_directory = os.path.abspath(working_directory) + context_path = os.path.join(working_directory, "context") + os.makedirs(context_path, exist_ok=True) + + if build_context_pk: + content_artifacts = ContentArtifact.objects.filter( + content__pulp_type="file.file", content__repositories__in=[build_context_pk] + ).order_by("-content__pulp_created") + for content_artifact in content_artifacts.select_related("artifact").iterator(): + if content_artifact.relative_path == containerfile_name: + containerfile_artifact = content_artifact.artifact + continue + _copy_file_from_artifact( + context_path, content_artifact.relative_path, content_artifact.artifact.file + ) + + containerfile_name = containerfile_name or "Containerfile" + _copy_file_from_artifact(working_directory, containerfile_name, containerfile_artifact) + containerfile_path = os.path.join(working_directory, containerfile_name) + + bud_cp = subprocess.run( + [ + "podman", + "build", + "-f", + containerfile_path, + "-t", + name, + context_path, + "--isolation", + "rootless", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if bud_cp.returncode != 0: + raise Exception(bud_cp.stderr) + image_dir = os.path.join(working_directory, "image") + os.makedirs(image_dir, exist_ok=True) + push_cp = subprocess.run( + ["podman", "push", "-f", "oci", name, "dir:{}".format(image_dir)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if push_cp.returncode != 0: + raise Exception(push_cp.stderr) + repository_version = add_image_from_directory_to_repository(image_dir, repository, tag) + if isinstance(containerfile_artifact, PulpTemporaryFile): + containerfile_artifact.delete() + + return repository_version + + def build_image_from_containerfile( containerfile_pk=None, artifacts=None, repository_pk=None, tag=None ): """ + DEPRECATED: this function is deprecated and will be removed in a future release. + Keeping it for now for backward compatibility. Builds an OCI container image from a Containerfile. The artifacts are made available inside the build container at the paths specified by their @@ -166,3 +259,12 @@ def build_image_from_containerfile( repository_version = add_image_from_directory_to_repository(image_dir, repository, tag) return repository_version + + +def _copy_file_from_artifact(context_path, relative_path, artifact): + dest_path = os.path.join(context_path, relative_path) + dirs = os.path.dirname(dest_path) + if dirs: + os.makedirs(dirs, exist_ok=True) + with open(dest_path, "wb") as dest: + shutil.copyfileobj(artifact.file, dest) diff --git a/pulp_container/app/viewsets.py b/pulp_container/app/viewsets.py index 8ae0bdcf4..67db707cb 100644 --- a/pulp_container/app/viewsets.py +++ b/pulp_container/app/viewsets.py @@ -7,7 +7,6 @@ import logging -from django.db import IntegrityError from django.db.models import Q from django_filters import CharFilter, MultipleChoiceFilter @@ -16,9 +15,9 @@ from rest_framework import mixins from rest_framework.decorators import action -from pulpcore.plugin.models import RepositoryVersion +from pulpcore.plugin.models import RepositoryVersion, PulpTemporaryFile from pulpcore.plugin.serializers import AsyncOperationResponseSerializer -from pulpcore.plugin.models import Artifact, Content +from pulpcore.plugin.models import Content from pulpcore.plugin.tasking import dispatch, general_multi_delete from pulpcore.plugin.util import ( extract_pk, @@ -696,6 +695,7 @@ class ContainerRepositoryViewSet( "condition": [ "has_model_or_obj_perms:container.build_image_containerrepository", "has_model_or_obj_perms:container.view_containerrepository", + "has_repo_or_repo_ver_param_model_or_obj_perms:file.view_filerepository", ], }, { @@ -937,26 +937,26 @@ def build_image(self, request, pk): ) serializer.is_valid(raise_exception=True) + serialized_data = serializer.deferred_files_validation(serializer.validated_data) - containerfile = serializer.validated_data["containerfile_artifact"] - try: - containerfile.save() - except IntegrityError: - containerfile = Artifact.objects.get(sha256=containerfile.sha256) - containerfile.touch() - tag = serializer.validated_data["tag"] + containerfile_tempfile_pk = None + if containerfile := serialized_data.get("containerfile", None): + temp_file = PulpTemporaryFile(file=containerfile) + temp_file.save() + containerfile_tempfile_pk = str(temp_file.pk) - artifacts = serializer.validated_data["artifacts"] - Artifact.objects.filter(pk__in=artifacts.keys()).touch() + containerfile_name = serialized_data.get("containerfile_name", None) + tag = serialized_data["tag"] result = dispatch( - tasks.build_image_from_containerfile, + tasks.build_image, exclusive_resources=[repository], kwargs={ - "containerfile_pk": str(containerfile.pk), + "containerfile_name": containerfile_name, + "containerfile_tempfile_pk": containerfile_tempfile_pk, "tag": tag, "repository_pk": str(repository.pk), - "artifacts": artifacts, + "build_context_pk": serialized_data.get("build_context_pk", None), }, ) return OperationPostponedResponse(result, request) diff --git a/pulp_container/tests/functional/api/test_build_images.py b/pulp_container/tests/functional/api/test_build_images.py index 5569dfa31..00574123d 100644 --- a/pulp_container/tests/functional/api/test_build_images.py +++ b/pulp_container/tests/functional/api/test_build_images.py @@ -2,16 +2,11 @@ from tempfile import NamedTemporaryFile -from pulp_smash.pulp3.utils import ( - gen_distribution, - gen_repo, -) +from pulpcore.tests.functional.utils import PulpTaskError +from pulp_smash.pulp3.utils import gen_distribution from pulp_smash.pulp3.bindings import monitor_task -from pulpcore.client.pulp_container import ( - ContainerContainerDistribution, - ContainerContainerRepository, -) +from pulpcore.client.pulp_container import ApiException, ContainerContainerDistribution @pytest.fixture @@ -19,7 +14,7 @@ def containerfile_name(): """A fixture for a basic container file used for building images.""" with NamedTemporaryFile() as containerfile: containerfile.write( - b"""FROM busybox:latest + b"""FROM quay.io/quay/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 @@ -29,35 +24,211 @@ def containerfile_name(): yield containerfile.name -def test_build_image( - pulpcore_bindings, - container_repository_api, +@pytest.fixture +def populated_file_repo( + containerfile_name, + file_bindings, + file_repo, + tmp_path_factory, +): + filename = tmp_path_factory.mktemp("fixtures") / "example.txt" + filename.write_bytes(b"test content") + upload_task = file_bindings.ContentFilesApi.create( + relative_path="foo/bar/example.txt", file=filename, repository=file_repo.pulp_href + ).task + monitor_task(upload_task) + + upload_task = file_bindings.ContentFilesApi.create( + relative_path="Containerfile", file=containerfile_name, repository=file_repo.pulp_href + ).task + monitor_task(upload_task) + + return file_repo + + +@pytest.fixture +def build_image(container_repository_api): + def _build_image(repository, containerfile=None, containerfile_name=None, build_context=None): + build_response = container_repository_api.build_image( + container_container_repository_href=repository, + containerfile=containerfile, + containerfile_name=containerfile_name or "", + build_context=build_context or "", + ) + monitor_task(build_response.task) + + return _build_image + + +def test_build_image_with_uploaded_containerfile( + build_image, + containerfile_name, container_distribution_api, + container_repo, + populated_file_repo, + delete_orphans_pre, gen_object_with_cleanup, - containerfile_name, local_registry, ): - """Test if a user can build an OCI image.""" - with NamedTemporaryFile() as text_file: - text_file.write(b"some text") - text_file.flush() - artifact = gen_object_with_cleanup(pulpcore_bindings.ArtifactsApi, text_file.name) + """Test build an OCI image from a file repository_version.""" + build_image( + repository=container_repo.pulp_href, + containerfile=containerfile_name, + build_context=f"{populated_file_repo.pulp_href}versions/1/", + ) - repository = gen_object_with_cleanup( - container_repository_api, ContainerContainerRepository(**gen_repo()) + distribution = gen_object_with_cleanup( + container_distribution_api, + ContainerContainerDistribution(**gen_distribution(repository=container_repo.pulp_href)), ) - artifacts = '{{"{}": "foo/bar/example.txt"}}'.format(artifact.pulp_href) - build_response = container_repository_api.build_image( - repository.pulp_href, containerfile=containerfile_name, artifacts=artifacts + local_registry.pull(distribution.base_path) + image = local_registry.inspect(distribution.base_path) + assert image[0]["Config"]["Cmd"] == ["cat", "/tmp/inside-image.txt"] + + +def test_build_image_from_repo_version_with_anon_user( + build_image, + containerfile_name, + container_repo, + delete_orphans_pre, + populated_file_repo, + gen_user, +): + """Test if a user without permission to file repo can build an OCI image.""" + user_helpless = gen_user( + model_roles=[ + "container.containerdistribution_collaborator", + "container.containerrepository_content_manager", + ] + ) + with user_helpless, pytest.raises(ApiException): + build_image( + container_repo.pulp_href, + containerfile_name, + build_context=f"{populated_file_repo.pulp_href}versions/1/", + ) + + +def test_build_image_from_repo_version_with_creator_user( + build_image, + containerfile_name, + container_repo, + delete_orphans_pre, + populated_file_repo, + gen_user, +): + """Test if a user (with the expected permissions) can build an OCI image.""" + user = gen_user( + object_roles=[ + ("container.containerrepository_content_manager", container_repo.pulp_href), + ("file.filerepository_viewer", populated_file_repo.pulp_href), + ], + ) + with user: + build_image( + repository=container_repo.pulp_href, + containerfile=containerfile_name, + build_context=f"{populated_file_repo.pulp_href}versions/1/", + ) + + +def test_build_image_without_containerfile( + build_image, + container_repo, + populated_file_repo, +): + """Test build an OCI image without a containerfile""" + with pytest.raises(ApiException): + build_image( + repository=container_repo.pulp_href, + build_context=f"{populated_file_repo.pulp_href}versions/2/", + ) + + +def test_build_image_without_expected_files( + build_image, + containerfile_name, + container_repo, +): + """ + Test build an OCI image without the expected files (build_context) defined in the Containerfile + """ + with pytest.raises(PulpTaskError): + build_image( + repository=container_repo.pulp_href, + containerfile=containerfile_name, + ) + + +def test_build_image_from_containerfile_name( + build_image, + container_distribution_api, + container_repo, + delete_orphans_pre, + gen_object_with_cleanup, + local_registry, + populated_file_repo, +): + """Test build an OCI image with a containerfile from build_context.""" + build_image( + repository=container_repo.pulp_href, + containerfile_name="Containerfile", + build_context=f"{populated_file_repo.pulp_href}versions/2/", ) - monitor_task(build_response.task) distribution = gen_object_with_cleanup( container_distribution_api, - ContainerContainerDistribution(**gen_distribution(repository=repository.pulp_href)), + ContainerContainerDistribution(**gen_distribution(repository=container_repo.pulp_href)), ) local_registry.pull(distribution.base_path) image = local_registry.inspect(distribution.base_path) assert image[0]["Config"]["Cmd"] == ["cat", "/tmp/inside-image.txt"] + + +def test_invalid_containerfile_from_build_context( + build_image, + container_repo, + populated_file_repo, +): + """Test with a non-existing Containerfile in file repository.""" + with pytest.raises(ApiException) as e: + build_image( + repository=container_repo.pulp_href, + containerfile_name="Non_existing_file", + build_context=f"{populated_file_repo.pulp_href}versions/2/", + ) + assert e.value.status == 400 + assert "Could not find the Containerfile" in e.value.body + + +def test_without_build_context( + build_image, container_distribution_api, container_repo, gen_object_with_cleanup, local_registry +): + """Test build with only a Containerfile (no additional files)""" + + def containerfile_without_context_files(): + with NamedTemporaryFile() as containerfile: + containerfile.write( + b"""FROM quay.io/quay/busybox:latest +# Print the content of the file when the container starts +CMD ["ls", "/"]""" + ) + containerfile.flush() + yield containerfile.name + + containerfile_name = containerfile_without_context_files() + build_image( + repository=container_repo.pulp_href, + containerfile=next(containerfile_name), + ) + + distribution = gen_object_with_cleanup( + container_distribution_api, + ContainerContainerDistribution(**gen_distribution(repository=container_repo.pulp_href)), + ) + + local_registry.pull(distribution.base_path) + image = local_registry.inspect(distribution.base_path) + assert image[0]["Config"]["Cmd"] == ["ls", "/"]