diff --git a/CHANGES/1296.misc b/CHANGES/1296.misc new file mode 100644 index 000000000..40a75dfd6 --- /dev/null +++ b/CHANGES/1296.misc @@ -0,0 +1 @@ +Created a new artifactless ConfigBlob model. diff --git a/pulp_container/app/content.py b/pulp_container/app/content.py index fcdc7786f..adf92b486 100644 --- a/pulp_container/app/content.py +++ b/pulp_container/app/content.py @@ -8,7 +8,7 @@ app.add_routes( [ web.get( - r"/pulp/container/{path:.+}/{content:(blobs|manifests)}/sha256:{digest:.+}", + r"/pulp/container/{path:.+}/{content:(blobs|manifests|config-blobs)}/sha256:{digest:.+}", registry.get_by_digest, ) ] diff --git a/pulp_container/app/management/commands/container-handle-image-data.py b/pulp_container/app/management/commands/container-handle-image-data.py index ed10e7547..1f70c1323 100644 --- a/pulp_container/app/management/commands/container-handle-image-data.py +++ b/pulp_container/app/management/commands/container-handle-image-data.py @@ -1,3 +1,4 @@ +import json from json.decoder import JSONDecodeError from gettext import gettext as _ @@ -6,8 +7,9 @@ from django.core.exceptions import ObjectDoesNotExist from django.core.management import BaseCommand +from django.db import IntegrityError, transaction -from pulp_container.app.models import Manifest +from pulp_container.app.models import Manifest, ConfigBlob from pulp_container.constants import MEDIA_TYPE @@ -64,6 +66,9 @@ def update_manifests(self, manifests_qs): ) manifests_updated_count += len(manifests_to_update) manifests_to_update.clear() + + self.update_config_blob(manifest) + if manifests_to_update: fields_to_update = ["annotations", "labels", "is_bootable", "is_flatpak"] manifests_qs.model.objects.bulk_update( @@ -73,3 +78,28 @@ def update_manifests(self, manifests_qs): manifests_updated_count += len(manifests_to_update) return manifests_updated_count + + def update_config_blob(self, manifest): + raw_manifest = manifest.config_blob._artifacts.get().file.read().decode("utf-8") + config_blob = json.loads(raw_manifest) + digest = manifest.config_blob.digest + with transaction.atomic(): + try: + blob = ConfigBlob( + data=raw_manifest, + digest=digest, + architecture=config_blob.get("architecture"), + os=config_blob.get("os"), + created=config_blob.get("created"), + author=config_blob.get("author", ""), + os_version=config_blob.get("os_version", ""), + os_features=config_blob.get("os_features", ""), + variant=config_blob.get("variant", ""), + rootfs=config_blob.get("rootfs", {}), + config=config_blob.get("config", {}), + history=config_blob.get("history", {}), + ) + blob.save() + except IntegrityError: + blob = ConfigBlob.objects.get(digest=digest) + blob.touch() diff --git a/pulp_container/app/migrations/0039_artifactless_config_blob.py b/pulp_container/app/migrations/0039_artifactless_config_blob.py new file mode 100644 index 000000000..318916f92 --- /dev/null +++ b/pulp_container/app/migrations/0039_artifactless_config_blob.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.10 on 2024-04-03 10:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('container', '0038_add_manifest_metadata_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='manifest', + name='config_blob', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='config', to='container.blob'), + ), + migrations.CreateModel( + name='ConfigBlob', + fields=[ + ('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.content')), + ('data', models.TextField()), + ('architecture', models.TextField()), + ('os', models.TextField()), + ('rootfs', models.JSONField(default=dict)), + ('digest', models.TextField(db_index=True)), + ('created', models.DateTimeField()), + ('author', models.TextField(blank=True, default='')), + ('os_version', models.TextField(blank=True, default='')), + ('os_features', models.TextField(blank=True, default='')), + ('variant', models.TextField(blank=True, default='')), + ('config', models.JSONField(default=dict)), + ('history', models.JSONField(default=dict)), + ], + options={ + 'default_related_name': '%(app_label)s_%(model_name)s', + 'unique_together': {('digest',)}, + }, + bases=('core.content',), + ), + migrations.AddField( + model_name='containerpushrepository', + name='pending_config_blobs', + field=models.ManyToManyField(to='container.configblob'), + ), + migrations.AddField( + model_name='containerrepository', + name='pending_config_blobs', + field=models.ManyToManyField(to='container.configblob'), + ), + migrations.AddField( + model_name='manifest', + name='config', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='config_blob', to='container.configblob'), + ), + ] diff --git a/pulp_container/app/modelresource.py b/pulp_container/app/modelresource.py index dfd7e8f85..7e3119f29 100644 --- a/pulp_container/app/modelresource.py +++ b/pulp_container/app/modelresource.py @@ -4,6 +4,7 @@ from pulp_container.app.models import ( Blob, + ConfigBlob, ContainerRepository, ContainerPushRepository, Manifest, @@ -60,6 +61,24 @@ class Meta: exclude = RepositoryResource.Meta.exclude + ("manifest_signing_service",) +class ConfigBlobResource(BaseContentResource): + """ + Resource for import/export of configblob entities + """ + + def set_up_queryset(self): + """ + :return: ConfigBlobs specific to a specified repo-version. + """ + return ConfigBlob.objects.filter(pk__in=self.repo_version.content).order_by( + "content_ptr_id" + ) + + class Meta: + model = ConfigBlob + import_id_fields = model.natural_key_fields() + + class BlobResource(BaseContentResource): """ Resource for import/export of blob entities @@ -91,6 +110,11 @@ class ManifestResource(BaseContentResource): attribute="config_blob", widget=widgets.ForeignKeyWidget(Blob, field="digest"), ) + config = fields.Field( + column_name="config", + attribute="config", + widget=widgets.ForeignKeyWidget(ConfigBlob, field="digest"), + ) def set_up_queryset(self): """ @@ -179,6 +203,7 @@ class Meta: IMPORT_ORDER = [ + ConfigBlobResource, BlobResource, ManifestResource, ManifestListManifestResource, diff --git a/pulp_container/app/models.py b/pulp_container/app/models.py index 123229581..6835792e6 100644 --- a/pulp_container/app/models.py +++ b/pulp_container/app/models.py @@ -38,6 +38,48 @@ logger = getLogger(__name__) +class ConfigBlob(Content): + """ + The manifest for image configuration. + + Fields: + created (models.DateTimeField): An combined date and time at which the image was created. + author (models.TextField): Name and/or email of the person or entity which created and is + responsible for maintaining the image. + architecture (models.TextField): The platform architecture. + os (models.TextField): The platform OS name. + os_version (models.TextField): The platform OS version. + os_features (models.TextField): The platform OS features. + variant (models.TextField): The platform variant. + config (models.ForeignKey): + rootfs (models.ForeignKey): + history (models.ForeignKey): + """ + + PROTECTED_FROM_RECLAIM = False + + TYPE = "config-blob" + + data = models.TextField() + architecture = models.TextField() + os = models.TextField() + rootfs = models.JSONField(default=dict) + digest = models.TextField(db_index=True) + + created = models.DateTimeField() + author = models.TextField(default="", blank=True) + os_version = models.TextField(default="", blank=True) + os_features = models.TextField(default="", blank=True) + variant = models.TextField(default="", blank=True) + + config = models.JSONField(default=dict) + history = models.JSONField(default=dict) + + class Meta: + default_related_name = "%(app_label)s_%(model_name)s" + unique_together = ("digest",) + + class Blob(Content): """ A blob defined within a manifest. @@ -107,7 +149,10 @@ class Manifest(Content): blobs = models.ManyToManyField(Blob, through="BlobManifest") config_blob = models.ForeignKey( - Blob, related_name="config_blob", null=True, on_delete=models.CASCADE + Blob, related_name="config", null=True, on_delete=models.CASCADE + ) + config = models.ForeignKey( + ConfigBlob, related_name="config_blob", null=True, on_delete=models.CASCADE ) # Order matters for through fields, (source, target) @@ -137,11 +182,10 @@ def init_annotations(self, manifest_data=None): return bool(self.annotations) def init_labels(self): - if self.config_blob: - config_artifact = self.config_blob._artifacts.get() - - config_data, _ = get_content_data(config_artifact) - self.labels = config_data.get("config", {}).get("Labels") or {} + if self.config and "Labels" in self.config.config.keys(): + self.labels = self.config.config["Labels"] or {} + else: + self.labels = {} return bool(self.labels) @@ -577,7 +621,7 @@ class ContainerRepository( """ TYPE = "container" - CONTENT_TYPES = [Blob, Manifest, Tag, ManifestSignature] + CONTENT_TYPES = [Blob, ConfigBlob, Manifest, Tag, ManifestSignature] REMOTE_TYPES = [ContainerRemote] PUSH_ENABLED = False @@ -585,6 +629,7 @@ class ContainerRepository( ManifestSigningService, on_delete=models.SET_NULL, null=True ) pending_blobs = models.ManyToManyField(Blob) + pending_config_blobs = models.ManyToManyField(ConfigBlob) pending_manifests = models.ManyToManyField(Manifest) class Meta: @@ -617,6 +662,7 @@ def remove_pending_content(self, repository_version): base_version=repository_version.base_version ).values_list("pk") self.pending_blobs.remove(*Blob.objects.filter(pk__in=added_content)) + self.pending_config_blobs.remove(*ConfigBlob.objects.filter(pk__in=added_content)) self.pending_manifests.remove(*Manifest.objects.filter(pk__in=added_content)) @@ -635,13 +681,14 @@ class ContainerPushRepository(Repository, AutoAddObjPermsMixin): """ TYPE = "container-push" - CONTENT_TYPES = [Blob, Manifest, Tag, ManifestSignature] + CONTENT_TYPES = [Blob, ConfigBlob, Manifest, Tag, ManifestSignature] PUSH_ENABLED = True manifest_signing_service = models.ForeignKey( ManifestSigningService, on_delete=models.SET_NULL, null=True ) pending_blobs = models.ManyToManyField(Blob) + pending_config_blobs = models.ManyToManyField(ConfigBlob) pending_manifests = models.ManyToManyField(Manifest) class Meta: @@ -671,6 +718,7 @@ def remove_pending_content(self, repository_version): base_version=repository_version.base_version ).values_list("pk") self.pending_blobs.remove(*Blob.objects.filter(pk__in=added_content)) + self.pending_config_blobs.remove(*ConfigBlob.objects.filter(pk__in=added_content)) self.pending_manifests.remove(*Manifest.objects.filter(pk__in=added_content)) diff --git a/pulp_container/app/redirects.py b/pulp_container/app/redirects.py index d88cbe093..0010bbb38 100644 --- a/pulp_container/app/redirects.py +++ b/pulp_container/app/redirects.py @@ -4,6 +4,7 @@ from django.shortcuts import redirect from pulp_container.app.exceptions import ManifestNotFound +from pulp_container.app.models import ConfigBlob from pulp_container.app.utils import get_accepted_media_types from pulp_container.constants import BLOB_CONTENT_TYPE, MEDIA_TYPE @@ -58,6 +59,9 @@ def issue_blob_redirect(self, blob): """ Issue a redirect for the passed blob. """ + if isinstance(blob, ConfigBlob): + return self.redirect_to_content_app("config-blobs", blob.digest) + return self.redirect_to_content_app("blobs", blob.digest) diff --git a/pulp_container/app/registry.py b/pulp_container/app/registry.py index 007b789a4..18996fc80 100644 --- a/pulp_container/app/registry.py +++ b/pulp_container/app/registry.py @@ -1,4 +1,5 @@ import json +import time import logging import os @@ -23,7 +24,14 @@ from pulpcore.plugin.tasking import dispatch from pulp_container.app.cache import RegistryContentCache -from pulp_container.app.models import ContainerDistribution, Tag, Blob, Manifest, BlobManifest +from pulp_container.app.models import ( + ContainerDistribution, + Tag, + Blob, + Manifest, + BlobManifest, + ConfigBlob, +) from pulp_container.app.tasks import download_image_data from pulp_container.app.utils import ( calculate_digest, @@ -285,6 +293,7 @@ async def get_by_digest(self, request): # "/pulp/container/{path:.+}/{content:(blobs|manifests)}/sha256:{digest:.+}" content_type = request.match_info["content"] if content_type == "manifests": + time.sleep(10) raw_manifest, digest, media_type = await pull_downloader.download_manifest() headers = { "Content-Type": media_type, @@ -299,8 +308,12 @@ async def get_by_digest(self, request): blob = await pull_downloader.init_remote_blob() ca = await blob.contentartifact_set.afirst() return await self._stream_content_artifact(request, web.StreamResponse(), ca) + elif content_type == "config-blobs": + return await self._config_blob_response(digest) else: raise RuntimeError("Only blobs or manifests are supported by the parser.") + elif request.match_info["content"] == "config-blobs": + return await self._config_blob_response(digest) else: raise PathNotResolved(path) else: @@ -310,6 +323,16 @@ async def get_by_digest(self, request): else: return await self._stream_content_artifact(request, web.StreamResponse(), ca) + async def _config_blob_response(self, digest): + blob = await ConfigBlob.objects.aget(digest=digest) + media_type = MEDIA_TYPE.CONFIG_BLOB_OCI + headers = { + "Content-Type": media_type, + "Docker-Content-Digest": digest, + "Docker-Distribution-API-Version": "registry/2.0", + } + return web.Response(text=blob.data, headers=headers) + @staticmethod async def _empty_blob(): # fmt: off @@ -408,12 +431,13 @@ async def run_pipeline(self, saved_artifact): "tag_name": self.identifier, }, ) + time.sleep(5) async def init_pending_content(self, digest, manifest_data, media_type, artifact): if config := manifest_data.get("config", None): config_digest = config["digest"] - config_blob = await self.save_config_blob(config_digest) - await sync_to_async(self.repository.pending_blobs.add)(config_blob) + config_blob = await ConfigBlob.objects.aget(digest=config_digest) + await sync_to_async(self.repository.pending_config_blobs.add)(config_blob) else: config_blob = None @@ -423,7 +447,7 @@ async def init_pending_content(self, digest, manifest_data, media_type, artifact if manifest_data["mediaType"] in (MEDIA_TYPE.MANIFEST_V2, MEDIA_TYPE.MANIFEST_OCI) else 1, media_type=media_type, - config_blob=config_blob, + config=config_blob, ) # skip if media_type of schema1 @@ -481,31 +505,3 @@ async def save_blob(self, digest, manifest): await ra.asave() return blob - - async def save_config_blob(self, config_digest): - blob_relative_url = "/v2/{name}/blobs/{digest}".format( - name=self.remote.namespaced_upstream_name, digest=config_digest - ) - blob_url = urljoin(self.remote.url, blob_relative_url) - downloader = self.remote.get_downloader(url=blob_url) - response = await downloader.run() - - response.artifact_attributes["file"] = response.path - saved_artifact = await save_artifact(response.artifact_attributes) - - config_blob = Blob(digest=config_digest) - try: - await config_blob.asave() - except IntegrityError: - config_blob = await Blob.objects.aget(digest=config_digest) - await sync_to_async(config_blob.touch)() - - content_artifact = ContentArtifact( - content=config_blob, - artifact=saved_artifact, - relative_path=config_digest, - ) - with suppress(IntegrityError): - await content_artifact.asave() - - return config_blob diff --git a/pulp_container/app/registry_api.py b/pulp_container/app/registry_api.py index 365aa7a0f..94f085d68 100644 --- a/pulp_container/app/registry_api.py +++ b/pulp_container/app/registry_api.py @@ -607,18 +607,16 @@ def get(self, request): tag.name, tag.tagged_manifest, req_oss, req_architectures, manifests ) for manifest, tagged in manifests.items(): - with storage.open(manifest.config_blob._artifacts.get().file.name) as file: - raw_data = file.read() - config_data = json.loads(raw_data) - labels = config_data.get("config", {}).get("Labels") + config_data = manifest.config + labels = config_data.config["Labels"] if not labels: continue if any(label not in labels.keys() for label in req_label_exists): continue - os = config_data["os"] + os = config_data.os if req_oss and os not in req_oss: continue - architecture = config_data["architecture"] + architecture = config_data.architecture if req_architectures and architecture not in req_architectures: continue if any( @@ -873,6 +871,29 @@ def partial_update(self, request, path, pk=None): return UploadResponse(upload=upload, path=path, request=request, status=204) + def create_config_blob(self, manifest, raw_manifest, digest): + with transaction.atomic(): + try: + blob = models.ConfigBlob( + data=raw_manifest, + digest=digest, + architecture=manifest.get("architecture"), + os=manifest.get("os"), + created=manifest.get("created"), + author=manifest.get("author", ""), + os_version=manifest.get("os_version", ""), + os_features=manifest.get("os_features", ""), + variant=manifest.get("variant", ""), + rootfs=manifest.get("rootfs", {}), + config=manifest.get("config", {}), + history=manifest.get("history", {}), + ) + blob.save() + except IntegrityError: + blob = models.ConfigBlob.objects.get(digest=digest) + blob.touch() + return blob + def put(self, request, path, pk=None): """ Create a blob from uploaded chunks. @@ -918,10 +939,17 @@ def put(self, request, path, pk=None): artifact = Artifact.objects.get(sha256=artifact.sha256) artifact.touch() - blob = self.create_blob(artifact, digest) + blob = None + try: + raw_manifest = artifact.file.read().decode("utf-8") + manifest = json.loads(raw_manifest) + blob = self.create_config_blob(manifest, raw_manifest, digest) + repository.pending_config_blobs.add(blob) + except UnicodeDecodeError: + blob = self.create_blob(artifact, digest) + repository.pending_blobs.add(blob) upload.delete() - repository.pending_blobs.add(blob) return BlobResponse(blob, path, 201, request) @@ -979,12 +1007,20 @@ def handle_safe_method(self, request, path, pk): except models.Blob.DoesNotExist: if pk == EMPTY_BLOB: return redirects.redirect_to_content_app("blobs", pk) - repository = repository.cast() try: - blob = repository.pending_blobs.get(digest=pk) - blob.touch() - except models.Blob.DoesNotExist: - raise BlobNotFound(digest=pk) + blob = models.ConfigBlob.objects.get(digest=pk) + return redirects.redirect_to_content_app("config-blobs", pk) + except models.ConfigBlob.DoesNotExist: + repository = repository.cast() + try: + blob = repository.pending_blobs.get(digest=pk) + blob.touch() + except models.Blob.DoesNotExist: + try: + blob = repository.pending_config_blobs.get(digest=pk) + blob.touch() + except: + raise BlobNotFound(digest=pk) return redirects.issue_blob_redirect(blob) @@ -1087,14 +1123,16 @@ def get_content_units_to_add(self, manifest, tag): models.MEDIA_TYPE.MANIFEST_LIST, models.MEDIA_TYPE.INDEX_OCI, ): - for listed_manifest in manifest.listed_manifests: + for listed_manifest in manifest.listed_manifests.all(): add_content_units.append(listed_manifest.pk) + add_content_units.append(listed_manifest.config_id) add_content_units.append(listed_manifest.config_blob_id) add_content_units.extend(listed_manifest.blobs.values_list("pk", flat=True)) elif manifest.media_type in ( models.MEDIA_TYPE.MANIFEST_V2, models.MEDIA_TYPE.MANIFEST_OCI, ): + add_content_units.append(manifest.config_id) add_content_units.append(manifest.config_blob_id) add_content_units.extend(manifest.blobs.values_list("pk", flat=True)) else: @@ -1152,7 +1190,10 @@ def put(self, request, path, pk=None): latest_version_content_pks = repository.latest_version().content.values_list("pk") manifests_pks = repository.pending_manifests.values_list("pk") blobs_pks = repository.pending_blobs.values_list("pk") - content_pks = latest_version_content_pks.union(manifests_pks).union(blobs_pks) + config_blobs_pks = repository.pending_config_blobs.values_list("pk") + content_pks = ( + latest_version_content_pks.union(manifests_pks).union(blobs_pks).union(config_blobs_pks) + ) found_manifests = models.Manifest.objects.none() @@ -1200,7 +1241,7 @@ def put(self, request, path, pk=None): digest__in=found_manifests.values_list("blobs__digest"), pk__in=content_pks, ) - found_config_blobs = models.Blob.objects.filter( + found_config_blobs = models.ConfigBlob.objects.filter( digest__in=found_manifests.values_list("config_blob__digest"), pk__in=content_pks, ) @@ -1214,9 +1255,7 @@ def put(self, request, path, pk=None): ) config_digest = config_layer.get("digest") - found_config_blobs = models.Blob.objects.filter( - digest=config_digest, pk__in=content_pks - ) + found_config_blobs = models.ConfigBlob.objects.filter(digest=config_digest) if not found_config_blobs.exists(): raise BlobInvalid(digest=config_digest) @@ -1311,7 +1350,7 @@ def _init_manifest(self, manifest_digest, media_type, config_blob=None): digest=manifest_digest, schema_version=2, media_type=media_type, - config_blob=config_blob, + config=config_blob, ) def _save_manifest(self, manifest, artifact): diff --git a/pulp_container/app/serializers.py b/pulp_container/app/serializers.py index a1a5f5150..fe1a555cd 100644 --- a/pulp_container/app/serializers.py +++ b/pulp_container/app/serializers.py @@ -83,6 +83,13 @@ class ManifestSerializer(SingleArtifactContentSerializer): view_name="container-blobs-detail", queryset=models.Blob.objects.all(), ) + config = DetailRelatedField( + many=False, + required=False, + help_text="Blob that contains configuration for this Manifest", + view_name="container-config-blobs-detail", + queryset=models.ConfigBlob.objects.all(), + ) annotations = serializers.JSONField( read_only=True, @@ -111,6 +118,7 @@ class Meta: "media_type", "listed_manifests", "config_blob", + "config", "blobs", "annotations", "labels", @@ -132,6 +140,34 @@ class Meta: model = models.Blob +class ConfigBlobSerializer(NoArtifactContentSerializer): + """ + Serializer for Blob Config Manifest + """ + + architecture = serializers.CharField( + help_text="The CPU architecture which the binaries in this image are built to run on" + ) + os = serializers.CharField( + help_text="The name of the operating system which the image is built to run on" + ) + created = serializers.DateTimeField(help_text="Date and time at which the image was created") + author = serializers.CharField( + help_text="The name and/or email address of the person or entity which created and is responsible for maintaining the image" + ) + digest = serializers.CharField(help_text="sha256 digest of the signature blob") + + class Meta: + fields = NoArtifactContentSerializer.Meta.fields + ( + "architecture", + "os", + "digest", + "created", + "author", + ) + model = models.ConfigBlob + + class ManifestSignatureSerializer(NoArtifactContentSerializer): """ Serializer for image manifest signatures. diff --git a/pulp_container/app/tasks/builder.py b/pulp_container/app/tasks/builder.py index f18d574ac..f11215e7b 100644 --- a/pulp_container/app/tasks/builder.py +++ b/pulp_container/app/tasks/builder.py @@ -8,6 +8,7 @@ from pulp_container.app.models import ( Blob, BlobManifest, + ConfigBlob, ContainerRepository, Manifest, Tag, @@ -34,15 +35,36 @@ def get_or_create_blob(layer_json, manifest, path): blob.touch() except Blob.DoesNotExist: layer_file_name = os.path.join(path, layer_json["digest"][7:]) - layer_artifact = Artifact.init_and_validate(layer_file_name) - layer_artifact.save() blob = Blob(digest=layer_json["digest"]) blob.save() - ContentArtifact( - 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() + if layer_json["mediaType"] in [MEDIA_TYPE.CONFIG_BLOB_OCI, MEDIA_TYPE.CONFIG_BLOB]: + with open(layer_file_name, "r") as content_file: + raw_data = content_file.read() + content_data = json.loads(raw_data) + config_blob = ConfigBlob( + data=raw_data, + digest=layer_json["digest"], + architecture=content_data.get("architecture"), + os=content_data.get("os"), + created=content_data.get("created"), + author=content_data.get("author", ""), + os_version=content_data.get("os_version", ""), + os_features=content_data.get("os_features", ""), + variant=content_data.get("variant", ""), + rootfs=content_data.get("rootfs", {}), + config=content_data.get("config", {}), + history=content_data.get("history", {}), + ) + config_blob.save() + return config_blob + else: + layer_artifact = Artifact.init_and_validate(layer_file_name) + layer_artifact.save() + ContentArtifact( + artifact=layer_artifact, content=blob, relative_path=layer_json["digest"] + ).save() + BlobManifest(manifest=manifest, manifest_blob=blob).save() + return blob @@ -79,7 +101,7 @@ def add_image_from_directory_to_repository(path, repository, tag): manifest_artifact.file.close() config_blob = get_or_create_blob(manifest_json["config"], manifest, path) - manifest.config_blob = config_blob + manifest.config = config_blob manifest.save() pks_to_add = [] diff --git a/pulp_container/app/tasks/recursive_add.py b/pulp_container/app/tasks/recursive_add.py index 977fb8e70..4279a614c 100644 --- a/pulp_container/app/tasks/recursive_add.py +++ b/pulp_container/app/tasks/recursive_add.py @@ -1,5 +1,6 @@ from pulp_container.app.models import ( Blob, + ConfigBlob, ContainerRepository, Manifest, MEDIA_TYPE, @@ -57,12 +58,13 @@ def recursive_add_content(repository_pk, content_units): ) ) - blobs_to_add = ( - Blob.objects.filter(pk__in=content_units) - | Blob.objects.filter(pk__in=manifests_to_add.values_list("blobs", flat=True)) - | Blob.objects.filter(pk__in=manifests_to_add.values_list("config_blob", flat=True)) + blobs_to_add = Blob.objects.filter(pk__in=content_units) | Blob.objects.filter( + pk__in=manifests_to_add.values_list("blobs", flat=True) ) + config_blobs_to_add = ConfigBlob.objects.filter( + pk__in=manifests_to_add.values_list("config", flat=True) + ) latest_version = repository.latest_version() if latest_version: tags_in_repo = latest_version.content.filter(pulp_type=Tag.get_pulp_type()) @@ -78,3 +80,4 @@ def recursive_add_content(repository_pk, content_units): new_version.add_content(manifest_lists_to_add) new_version.add_content(manifests_to_add) new_version.add_content(blobs_to_add) + new_version.add_content(config_blobs_to_add) diff --git a/pulp_container/app/tasks/recursive_remove.py b/pulp_container/app/tasks/recursive_remove.py index f20b3b71d..8370f46df 100644 --- a/pulp_container/app/tasks/recursive_remove.py +++ b/pulp_container/app/tasks/recursive_remove.py @@ -3,6 +3,7 @@ from pulp_container.app.models import ( Blob, + ConfigBlob, Manifest, ManifestSignature, MEDIA_TYPE, @@ -54,6 +55,7 @@ def recursive_remove_content(repository_pk, content_units): ] type_manifest = Q(media_type__in=manifest_media_types) blobs_in_repo = Q(pk__in=latest_content.filter(pulp_type=Blob.get_pulp_type())) + config_blobs_in_repo = Q(pk__in=latest_content.filter(pulp_type=ConfigBlob.get_pulp_type())) # Tags do not have must_remain because they are the highest level content. tags_to_remove = Tag.objects.filter(user_provided_content & tags_in_repo) @@ -100,11 +102,13 @@ def recursive_remove_content(repository_pk, content_units): pk__in=manifests_to_remove ) - listed_blobs_must_remain = Q( - pk__in=manifests_to_remain.values_list("blobs", flat=True) - ) | Q(pk__in=manifests_to_remain.values_list("config_blob", flat=True)) - listed_blobs_to_remove = Q(pk__in=manifests_to_remove.values_list("blobs", flat=True)) | Q( - pk__in=manifests_to_remove.values_list("config_blob", flat=True) + listed_blobs_must_remain = Q(pk__in=manifests_to_remain.values_list("blobs", flat=True)) + listed_config_blobs_must_remain = Q( + pk__in=manifests_to_remain.values_list("config", flat=True) + ) + listed_blobs_to_remove = Q(pk__in=manifests_to_remove.values_list("blobs", flat=True)) + listed_config_blobs_to_remove = Q( + pk__in=manifests_to_remove.values_list("config", flat=True) ) blobs_to_remove = ( @@ -112,6 +116,11 @@ def recursive_remove_content(repository_pk, content_units): .filter(blobs_in_repo) .exclude(listed_blobs_must_remain) ) + config_blobs_to_remove = ( + ConfigBlob.objects.filter(user_provided_content | listed_config_blobs_to_remove) + .filter(config_blobs_in_repo) + .exclude(listed_config_blobs_must_remain) + ) # signatures can't be shared, so no need to calculate which ones to remain sigs_to_remove_from_manifests = Q(signed_manifest__in=manifests_to_remove) @@ -124,4 +133,5 @@ def recursive_remove_content(repository_pk, content_units): new_version.remove_content(manifest_lists_to_remove) new_version.remove_content(manifests_to_remove) new_version.remove_content(blobs_to_remove) + new_version.remove_content(config_blobs_to_remove) new_version.remove_content(signatures_to_remove) diff --git a/pulp_container/app/tasks/sync_stages.py b/pulp_container/app/tasks/sync_stages.py index 000a13b0e..8367cb6ea 100644 --- a/pulp_container/app/tasks/sync_stages.py +++ b/pulp_container/app/tasks/sync_stages.py @@ -23,6 +23,7 @@ from pulp_container.app.models import ( Blob, BlobManifest, + ConfigBlob, Manifest, ManifestListManifest, ManifestSignature, @@ -270,7 +271,7 @@ async def resolve_flush(self): for manifest_dc in self.manifest_dcs: config_blob_dc = manifest_dc.extra_data.get("config_blob_dc") if config_blob_dc: - manifest_dc.content.config_blob = await config_blob_dc.resolution() + manifest_dc.content.config = await config_blob_dc.resolution() await sync_to_async(manifest_dc.content.init_labels)() manifest_dc.content.init_image_nature() for blob_dc in manifest_dc.extra_data["blob_dcs"]: @@ -355,9 +356,9 @@ async def handle_blobs(self, manifest_dc, content_data): await self.put(blob_dc) layer = content_data.get("config", None) if layer: - blob_dc = self.create_blob(layer, deferred_download=False) - manifest_dc.extra_data["config_blob_dc"] = blob_dc - await self.put(blob_dc) + config_blob_dc = await self.create_config_blob(layer) + manifest_dc.extra_data["config_blob_dc"] = config_blob_dc + await self.put(config_blob_dc) def create_tagged_manifest_list(self, tag_name, saved_artifact, manifest_list_data, media_type): """ @@ -515,6 +516,36 @@ async def create_listed_manifest(self, manifest_data): ) return {"manifest_dc": man_dc, "platform": platform, "content_data": content_data} + async def create_config_blob(self, blob_data): + digest = blob_data.get("digest") or blob_data.get("blobSum") + relative_url = "/v2/{name}/blobs/{digest}".format( + name=self.remote.namespaced_upstream_name, digest=digest + ) + blob_url = urljoin(self.remote.url, relative_url) + + downloader = self.remote.get_downloader(url=blob_url) + response = await downloader.run(extra_data={"headers": V2_ACCEPT_HEADERS}) + with open(response.path, "rb") as content_file: + raw_data = content_file.read() + content_data = json.loads(raw_data) + config_blob = ConfigBlob( + data=raw_data.decode("utf-8"), + digest=digest, + architecture=content_data.get("architecture"), + os=content_data.get("os"), + created=content_data.get("created"), + author=content_data.get("author", ""), + os_version=content_data.get("os_version", ""), + os_features=content_data.get("os_features", ""), + variant=content_data.get("variant", ""), + rootfs=content_data.get("rootfs", {}), + config=content_data.get("config", {}), + history=content_data.get("history", {}), + ) + config_blob_dc = DeclarativeContent(content=config_blob) + + return config_blob_dc + def create_blob(self, blob_data, deferred_download=True): """ Create blob. diff --git a/pulp_container/app/viewsets.py b/pulp_container/app/viewsets.py index 5bba73fea..e3d138e8e 100644 --- a/pulp_container/app/viewsets.py +++ b/pulp_container/app/viewsets.py @@ -95,6 +95,18 @@ class Meta: } +class ConfigBlobFilter(ContentFilter): + """ + FilterSet for ConfigBlobs. + """ + + class Meta: + model = models.ConfigBlob + fields = { + "digest": ["exact", "in"], + } + + class ManifestSignatureFilter(ContentFilter): """ FilterSet for image signatures. @@ -317,6 +329,39 @@ class BlobViewSet(ContainerContentQuerySetMixin, ReadOnlyContentViewSet): } +class ConfigBlobViewSet(ContainerContentQuerySetMixin, ReadOnlyContentViewSet): + """ + ViewSet for ConfigBlobs. + """ + + endpoint_name = "config-blobs" + queryset = models.ConfigBlob.objects.all() + serializer_class = serializers.ConfigBlobSerializer + filterset_class = ConfigBlobFilter + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + ], + "queryset_scoping": { + "function": "get_content_qs", + "parameters": { + "push_perm": "container.view_containerdistribution", + "mirror_perm": "container.view_containerrepository", + }, + }, + } + + class ManifestSignatureViewSet(ContainerContentQuerySetMixin, ReadOnlyContentViewSet): """ ViewSet for image signatures. diff --git a/pulp_container/tests/functional/api/test_pulpimportexport.py b/pulp_container/tests/functional/api/test_pulpimportexport.py index 464b4d1e5..70b26d7cc 100644 --- a/pulp_container/tests/functional/api/test_pulpimportexport.py +++ b/pulp_container/tests/functional/api/test_pulpimportexport.py @@ -91,7 +91,7 @@ def test_import_export_standard( assert manifest.listed_manifests != [] else: assert manifest.blobs != [] - assert manifest.config_blob is not None + assert manifest.config is not None distribution_path = str(uuid.uuid4()) distribution = { @@ -177,7 +177,7 @@ def test_import_export_create_repositories( assert manifest.listed_manifests != [] else: assert manifest.blobs != [] - assert manifest.config_blob is not None + assert manifest.config is not None distribution = { "name": distribution_path, diff --git a/pulp_container/tests/functional/api/test_recursive_add.py b/pulp_container/tests/functional/api/test_recursive_add.py index 91d0171da..214963907 100644 --- a/pulp_container/tests/functional/api/test_recursive_add.py +++ b/pulp_container/tests/functional/api/test_recursive_add.py @@ -153,7 +153,8 @@ def test_copy_manifest_by_digest(self): self.assertFalse("container.tag" in to_repo_content) self.assertEqual(to_repo_content["container.manifest"]["count"], 1) # each manifest (non-list) has 3 blobs, 1 blob is shared - self.assertEqual(to_repo_content["container.blob"]["count"], 3) + self.assertEqual(to_repo_content["container.blob"]["count"], 2) + self.assertEqual(to_repo_content["container.config-blob"]["count"], 1) def test_copy_manifest_by_digest_and_media_type(self): """Specify a single manifest by digest to copy.""" @@ -181,7 +182,8 @@ def test_copy_manifest_by_digest_and_media_type(self): self.assertEqual(to_repo_content["container.manifest"]["count"], 1) # manifest_a has 3 blobs # 3rd blob is the parent blob from apline repo - self.assertEqual(to_repo_content["container.blob"]["count"], 3) + self.assertEqual(to_repo_content["container.blob"]["count"], 2) + self.assertEqual(to_repo_content["container.config-blob"]["count"], 1) def test_copy_all_manifest_lists_by_media_type(self): """Specify the media_type, to copy all manifest lists.""" @@ -203,7 +205,8 @@ def test_copy_all_manifest_lists_by_media_type(self): self.assertEqual(to_repo_content["container.manifest"]["count"], 9) # each manifest (non-list) has 3 blobs, 1 blob is shared # 11th blob is the parent blob from apline repo, which is shared by all other manifests - self.assertEqual(to_repo_content["container.blob"]["count"], 11) + self.assertEqual(to_repo_content["container.blob"]["count"], 6) + self.assertEqual(to_repo_content["container.config-blob"]["count"], 5) def test_copy_all_manifests_by_media_type(self): """Specify the media_type, to copy all manifest lists.""" @@ -225,7 +228,8 @@ def test_copy_all_manifests_by_media_type(self): self.assertEqual(to_repo_content["container.manifest"]["count"], 5) # each manifest (non-list) has 3 blobs, 1 blob is shared # 11th blob is the parent blob from apline repo, which is shared by all other manifests - self.assertEqual(to_repo_content["container.blob"]["count"], 11) + self.assertEqual(to_repo_content["container.blob"]["count"], 6) + self.assertEqual(to_repo_content["container.config-blob"]["count"], 5) def test_fail_to_copy_invalid_manifest_media_type(self): """Specify the media_type, to copy all manifest lists.""" @@ -296,7 +300,8 @@ def test_copy_multiple_manifests_by_digest(self): self.assertEqual(to_repo_content["container.manifest"]["count"], 6) # each manifest (non-list) has 3 blobs, 1 blob is shared # 9th blob is the parent blob from apline repo, which is shared by all other manifests - self.assertEqual(to_repo_content["container.blob"]["count"], 9) + self.assertEqual(to_repo_content["container.blob"]["count"], 5) + self.assertEqual(to_repo_content["container.config-blob"]["count"], 4) def test_copy_manifests_by_digest_empty_list(self): """Passing an empty list copies no manifests.""" @@ -424,7 +429,8 @@ def test_copy_tags_by_name(self): self.assertEqual(to_repo_content["container.manifest"]["count"], 4) # each manifest (non-list) has 3 blobs, 1 blob is shared # 7th blob is the parent blob from apline repo, which is shared by all other manifests - self.assertEqual(to_repo_content["container.blob"]["count"], 7) + self.assertEqual(to_repo_content["container.blob"]["count"], 4) + self.assertEqual(to_repo_content["container.config-blob"]["count"], 3) def test_copy_tags_by_name_empty_list(self): """Passing an empty list of names copies nothing.""" @@ -539,7 +545,8 @@ def test_manifest_recursion(self): # each manifest (non-list) has 3 blobs, 1 blob is shared self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 1) def test_manifest_list_recursion(self): """Add a Manifest List, related manifests, and related blobs.""" @@ -577,7 +584,8 @@ def test_tagged_manifest_list_recursion(self): self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 3) # each manifest (non-list) has 3 blobs, 1 blob is shared # 5th blob is the parent blob from apline repo, which is shared by all other manifests - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 5) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 2) def test_tagged_manifest_recursion(self): """Add a tagged manifest and its related blobs.""" @@ -595,7 +603,8 @@ def test_tagged_manifest_recursion(self): self.assertEqual(latest.content_summary.added["container.tag"]["count"], 1) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 1) def test_tag_replacement(self): """Add a tagged manifest to a repo with a tag of that name already in place.""" @@ -666,4 +675,5 @@ def test_many_tagged_manifest_lists(self): self.assertEqual(latest.content_summary.added["container.tag"]["count"], 4) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 9) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 11) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 6) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 5) diff --git a/pulp_container/tests/functional/api/test_recursive_remove.py b/pulp_container/tests/functional/api/test_recursive_remove.py index d12daf03d..cd2275f87 100644 --- a/pulp_container/tests/functional/api/test_recursive_remove.py +++ b/pulp_container/tests/functional/api/test_recursive_remove.py @@ -101,7 +101,8 @@ def test_remove_everything(self): # Ensure test begins in the correct state self.assertFalse("container.tag" in latest.content_summary.added) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 1) # Actual test remove_response = self.repositories_api.remove( @@ -111,7 +112,8 @@ def test_remove_everything(self): latest_version_href = self.repositories_api.read(self.to_repo.pulp_href).latest_version_href latest = self.versions_api.read(latest_version_href) self.assertEqual(latest.content_summary.present, {}) - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 1) self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 1) def test_remove_invalid_content_units(self): @@ -139,7 +141,8 @@ def test_manifest_recursion(self): # Ensure test begins in the correct state self.assertFalse("container.tag" in latest.content_summary.added) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 1) # Actual test remove_response = self.repositories_api.remove( @@ -150,7 +153,8 @@ def test_manifest_recursion(self): latest = self.versions_api.read(latest_version_href) self.assertFalse("container.tag" in latest.content_summary.removed) self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 1) def test_manifest_list_recursion(self): """Add a Manifest List, related manifests, and related blobs.""" @@ -167,7 +171,8 @@ def test_manifest_list_recursion(self): # Ensure test begins in the correct state self.assertFalse("container.tag" in latest.content_summary.added) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 3) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 5) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 2) # Actual test remove_response = self.repositories_api.remove( @@ -178,7 +183,8 @@ def test_manifest_list_recursion(self): latest = self.versions_api.read(latest_version_href) self.assertFalse("container.tag" in latest.content_summary.removed) self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 3) - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 5) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 2) def test_tagged_manifest_list_recursion(self): """Add a tagged manifest list, and its related manifests and blobs.""" @@ -197,7 +203,8 @@ def test_tagged_manifest_list_recursion(self): # Ensure test begins in the correct state self.assertEqual(latest.content_summary.added["container.tag"]["count"], 1) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 3) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 5) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 2) # Actual test remove_response = self.repositories_api.remove( @@ -208,7 +215,8 @@ def test_tagged_manifest_list_recursion(self): latest = self.versions_api.read(latest_version_href) self.assertEqual(latest.content_summary.removed["container.tag"]["count"], 1) self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 3) - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 5) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 2) def test_tagged_manifest_recursion(self): """Add a tagged manifest and its related blobs.""" @@ -227,7 +235,8 @@ def test_tagged_manifest_recursion(self): # Ensure valid starting state self.assertEqual(latest.content_summary.added["container.tag"]["count"], 1) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 1) # Actual test remove_response = self.repositories_api.remove( @@ -239,7 +248,8 @@ def test_tagged_manifest_recursion(self): self.assertEqual(latest.content_summary.removed["container.tag"]["count"], 1) self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 1) def test_manifests_shared_blobs(self): """Starting with 2 manifests that share blobs, remove one of them.""" @@ -264,7 +274,8 @@ def test_manifests_shared_blobs(self): self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 2) # manifest_a has 2 blobs, 1 config blob, and manifest_e has 3 blobs 1 config blob # manifest_a blobs are shared with manifest_e - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 5) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 2) # Actual test remove_response = self.repositories_api.remove( @@ -276,7 +287,8 @@ def test_manifests_shared_blobs(self): self.assertFalse("container.tag" in latest.content_summary.removed) self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 1) # Despite having 4 blobs, only 2 are removed, 2 is shared with manifest_a. - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 1) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 1) def test_manifest_lists_shared_manifests(self): """Starting with 2 manifest lists that share a manifest, remove one of them.""" @@ -301,7 +313,8 @@ def test_manifest_lists_shared_manifests(self): self.assertFalse("container.tag" in latest.content_summary.added) # 2 manifest lists, each with 2 manifests, 1 manifest shared self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 5) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 7) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 4) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 3) # Actual test remove_response = self.repositories_api.remove( @@ -313,7 +326,8 @@ def test_manifest_lists_shared_manifests(self): self.assertFalse("container.tag" in latest.content_summary.removed) # 1 manifest list, 1 manifest self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 2) - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 1) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 1) def test_many_tagged_manifest_lists(self): """Add several Manifest List, related manifests, and related blobs.""" @@ -346,7 +360,8 @@ def test_many_tagged_manifest_lists(self): self.assertEqual(latest.content_summary.added["container.tag"]["count"], 4) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 9) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 11) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 6) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 5) remove_response = self.repositories_api.remove( self.to_repo.pulp_href, {"content_units": [ml_i_tag, ml_ii_tag, ml_iii_tag, ml_iv_tag]} @@ -357,7 +372,8 @@ def test_many_tagged_manifest_lists(self): self.assertEqual(latest.content_summary.removed["container.tag"]["count"], 4) self.assertEqual(latest.content_summary.removed["container.manifest"]["count"], 9) - self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 11) + self.assertEqual(latest.content_summary.removed["container.blob"]["count"], 6) + self.assertEqual(latest.content_summary.removed["container.config-blob"]["count"], 5) def test_cannot_remove_tagged_manifest(self): """ @@ -374,7 +390,8 @@ def test_cannot_remove_tagged_manifest(self): latest = self.versions_api.read(latest_version_href) self.assertEqual(latest.content_summary.added["container.tag"]["count"], 1) self.assertEqual(latest.content_summary.added["container.manifest"]["count"], 1) - self.assertEqual(latest.content_summary.added["container.blob"]["count"], 3) + self.assertEqual(latest.content_summary.added["container.blob"]["count"], 2) + self.assertEqual(latest.content_summary.added["container.config-blob"]["count"], 1) remove_respone = self.repositories_api.remove( self.to_repo.pulp_href, {"content_units": [manifest_a_tag.tagged_manifest]} @@ -383,7 +400,12 @@ def test_cannot_remove_tagged_manifest(self): latest_version_href = self.repositories_api.read(self.to_repo.pulp_href).latest_version_href latest = self.versions_api.read(latest_version_href) - for content_type in ["container.tag", "container.manifest", "container.blob"]: + for content_type in [ + "container.tag", + "container.manifest", + "container.blob", + "container.config-blob", + ]: self.assertFalse(content_type in latest.content_summary.removed, msg=content_type) @@ -454,6 +476,10 @@ def test_remove_image(self): content_summary.removed["container.blob"]["count"], self.content_to_remove["container.blob"]["count"], ) + self.assertEqual( + content_summary.removed["container.config-blob"]["count"], + self.content_to_remove["container.config-blob"]["count"], + ) self.assertEqual( content_summary.removed["container.manifest"]["count"], self.content_to_remove["container.manifest"]["count"], diff --git a/pulp_container/tests/functional/api/test_token_authentication.py b/pulp_container/tests/functional/api/test_token_authentication.py index 682b88c30..c2badc68d 100644 --- a/pulp_container/tests/functional/api/test_token_authentication.py +++ b/pulp_container/tests/functional/api/test_token_authentication.py @@ -135,7 +135,7 @@ def compare_config_blob_digests(self, pulled_manifest_digest): tagged_manifest_href = tag_response[0]["tagged_manifest"] manifest_response = self.client.get(tagged_manifest_href) - config_blob_response = self.client.get(manifest_response["config_blob"]) + config_blob_response = self.client.get(manifest_response["config"]) self.assertEqual(pulled_manifest_digest, config_blob_response["digest"])