Skip to content

Commit

Permalink
Add repository_version param as a building context
Browse files Browse the repository at this point in the history
closes: #479
  • Loading branch information
git-hyagi committed Jul 30, 2024
1 parent ddfcca8 commit 7b030c8
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 86 deletions.
2 changes: 2 additions & 0 deletions CHANGES/479.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Replaced `artifacts` by `build_context` (i.e., a file plugin repository version HREF)
as the parameter to provide the build context for a Containerfile.
29 changes: 15 additions & 14 deletions docs/admin/guides/build-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,32 @@

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`.
string that maps artifacts in Pulp to a filename. Any files passed in (via `build_context`) are
available inside the build container at the path defined in File Content `relative-path`.

## Create a Repository
## 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

```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
Expand All @@ -41,12 +42,12 @@ CMD ["cat", "/inside-image.txt"]' >> Containerfile
## Build an OCI image

```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 :$CONTAINER_REPO'build_image/' "containerfile@./Containerfile" \
build_context=${FILE_REPO}versions/1/ | 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 on-demand policy will not automatically pull the missing artifacts.
Trying to build using a file that is not yet pulled will fail.
39 changes: 11 additions & 28 deletions pulp_container/app/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from gettext import gettext as _
import os
import re

from django.core.validators import URLValidator
Expand Down Expand Up @@ -774,11 +773,10 @@ 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,
)

def __init__(self, *args, **kwargs):
Expand All @@ -802,28 +800,13 @@ def validate(self, data):
raise serializers.ValidationError(
_("'containerfile' or 'containerfile_artifact' must " "be specified.")
)
artifacts = {}
if "artifacts" in data:
for url, relative_path in data["artifacts"].items():
if os.path.isabs(relative_path):
raise serializers.ValidationError(
_("Relative path cannot start with '/'. " "{0}").format(relative_path)
)
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

# the "has_repo_or_repo_ver_param_model_or_obj_perms" permission condition function expects
# a "repo" or "repository_version" arguments, so we need to pass "build_context" as
# "repository_version" to be able to validate the permissions
if data.get("build_context", None):
data["repository_version"] = data["build_context"]

return data

class Meta:
Expand All @@ -832,7 +815,7 @@ class Meta:
"containerfile",
"repository",
"tag",
"artifacts",
"build_context",
)


Expand Down
43 changes: 29 additions & 14 deletions pulp_container/app/tasks/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)
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, RepositoryVersion


def get_or_create_blob(layer_json, manifest, path):
Expand Down Expand Up @@ -96,7 +96,7 @@ def add_image_from_directory_to_repository(path, repository, tag):


def build_image_from_containerfile(
containerfile_pk=None, artifacts=None, repository_pk=None, tag=None
containerfile_pk=None, build_context_pk=None, repository_pk=None, tag=None
):
"""
Builds an OCI container image from a Containerfile.
Expand All @@ -106,11 +106,10 @@ def build_image_from_containerfile(
Args:
containerfile_pk (str): The pk of an Artifact that contains the Containerfile
artifacts (dict): A dictionary where each key is an artifact PK and the value is it's
relative path (name) inside the /pulp_working_directory of the build
container executing the Containerfile.
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
build_context_pk: The pk of a RepositoryVersion with the artifacts used in the build context
of the Containerfile.
Returns:
A class:`pulpcore.plugin.models.RepositoryVersion` that contains the new OCI container
Expand All @@ -124,16 +123,23 @@ def build_image_from_containerfile(
working_directory = os.path.abspath(working_directory)
context_path = os.path.join(working_directory, "context")
os.makedirs(context_path, exist_ok=True)
for key, val in artifacts.items():
artifact = Artifact.objects.get(pk=key)
dest_path = os.path.join(context_path, val)
dirs = os.path.split(dest_path)[0]
if dirs:
os.makedirs(dirs, exist_ok=True)
with open(dest_path, "wb") as dest:
shutil.copyfileobj(artifact.file, dest)

containerfile_path = os.path.join(working_directory, "Containerfile")
if build_context_pk:
build_context = RepositoryVersion.objects.get(pk=build_context_pk)
content_artifacts = ContentArtifact.objects.filter(
content__pulp_type="file.file", content__in=build_context.content
).order_by("-content__pulp_created")
for content_artifact in content_artifacts.select_related("artifact").iterator():
if not content_artifact.artifact:
raise RuntimeError(
"It is not possible to use File content synced with on-demand "
"policy without pulling the data first."
)
_copy_file_from_artifact(
context_path, content_artifact.relative_path, content_artifact.artifact.file
)

containerfile_path = os.path.join(working_directory, "Containerfile")

with open(containerfile_path, "wb") as dest:
shutil.copyfileobj(containerfile.file, dest)
Expand Down Expand Up @@ -166,3 +172,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)
6 changes: 3 additions & 3 deletions pulp_container/app/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,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",
],
},
{
Expand Down Expand Up @@ -946,8 +947,7 @@ def build_image(self, request, pk):
containerfile.touch()
tag = serializer.validated_data["tag"]

artifacts = serializer.validated_data["artifacts"]
Artifact.objects.filter(pk__in=artifacts.keys()).touch()
build_context = serializer.validated_data.get("build_context", None)

result = dispatch(
tasks.build_image_from_containerfile,
Expand All @@ -956,7 +956,7 @@ def build_image(self, request, pk):
"containerfile_pk": str(containerfile.pk),
"tag": tag,
"repository_pk": str(repository.pk),
"artifacts": artifacts,
"build_context_pk": build_context.pk,
},
)
return OperationPostponedResponse(result, request)
Expand Down
116 changes: 89 additions & 27 deletions pulp_container/tests/functional/api/test_build_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,18 @@

from tempfile import NamedTemporaryFile

from pulp_smash.pulp3.utils import (
gen_distribution,
gen_repo,
)
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
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
Expand All @@ -29,35 +23,103 @@ def containerfile_name():
yield containerfile.name


@pytest.fixture
def populated_file_repo(
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)

return file_repo


@pytest.fixture
def build_image(container_repository_api):
def _build_image(repository, containerfile, build_context=None):
build_response = container_repository_api.build_image(
container_container_repository_href=repository,
containerfile=containerfile,
build_context=build_context,
)
monitor_task(build_response.task)

return _build_image


def test_build_image(
pulpcore_bindings,
container_repository_api,
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)

repository = gen_object_with_cleanup(
container_repository_api, ContainerContainerRepository(**gen_repo())
)

artifacts = '{{"{}": "foo/bar/example.txt"}}'.format(artifact.pulp_href)
build_response = container_repository_api.build_image(
repository.pulp_href, containerfile=containerfile_name, artifacts=artifacts
"""Test build an OCI image from a file repository_version."""
build_image(
container_repo.pulp_href,
containerfile_name,
build_context=f"{populated_file_repo.pulp_href}versions/1/",
)
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_build_image_from_repo_version_with_anon_user(
build_image,
containerfile_name,
container_repo,
populated_file_repo,
delete_orphans_pre,
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,
populated_file_repo,
delete_orphans_pre,
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(
container_repo.pulp_href,
containerfile_name,
build_context=f"{populated_file_repo.pulp_href}versions/1/",
)

0 comments on commit 7b030c8

Please sign in to comment.