diff --git a/CHANGES/672.feature b/CHANGES/672.feature new file mode 100644 index 000000000..baed9b429 --- /dev/null +++ b/CHANGES/672.feature @@ -0,0 +1 @@ +Added validation for uploaded manifest JSON content. diff --git a/CHANGES/853.bugfix b/CHANGES/853.bugfix new file mode 100644 index 000000000..1a9153b84 --- /dev/null +++ b/CHANGES/853.bugfix @@ -0,0 +1,2 @@ +Fixed internal server errors raised when a podman client (<4.0) used invalid content types for +manifest lists. diff --git a/CHANGES/854.bugfix b/CHANGES/854.bugfix new file mode 100644 index 000000000..00e3aeb5d --- /dev/null +++ b/CHANGES/854.bugfix @@ -0,0 +1 @@ +Fixed a misleading error message raised when a user provided an invalid manifest list. diff --git a/pulp_container/app/json_schemas.py b/pulp_container/app/json_schemas.py index ca0ce7e01..df621612b 100644 --- a/pulp_container/app/json_schemas.py +++ b/pulp_container/app/json_schemas.py @@ -1,3 +1,5 @@ +from collections import namedtuple + SIGNATURE_SCHEMA = """{ "$schema": "http://json-schema.org/draft-07/schema", "$id": "https://example.com/product.schema.json", @@ -52,3 +54,490 @@ "required": ["critical", "optional"], "additionalProperties": false }""" + + + + +DESCRIPTOR_MEDIATYPE_KEY = "mediaType" +DESCRIPTOR_SIZE_KEY = "size" +DESCRIPTOR_DIGEST_KEY = "digest" +DESCRIPTOR_URLS_KEY = "urls" +DESCRIPTOR_ANNOTATIONS_KEY = "annotations" + + +def get_descriptor_schema( + allowed_media_types, additional_properties=None, additional_required=None +): + properties = { + DESCRIPTOR_MEDIATYPE_KEY: { + "type": "string", + "description": "The MIME type of the referenced manifest", + "enum": allowed_media_types, + }, + DESCRIPTOR_SIZE_KEY: { + "type": "number", + "description": "The size in bytes of the object. This field exists so that a " + + "client will have an expected size for the content before " + + "validating. If the length of the retrieved content does not " + + "match the specified length, the content should not be trusted.", + }, + DESCRIPTOR_DIGEST_KEY: { + "type": "string", + "description": "The content addressable digest of the manifest in the blob store", + }, + DESCRIPTOR_ANNOTATIONS_KEY: { + "type": "object", + "description": "The annotations, if any, on this descriptor", + "additionalProperties": True, + }, + DESCRIPTOR_URLS_KEY: { + "type": "array", + "description": "This OPTIONAL property specifies a list of URIs from which this object MAY be downloaded. Each entry MUST conform to RFC 3986. Entries SHOULD use the http and https schemes, as defined in RFC 7230.", + "items": { + "type": "string", + }, + }, + } + + if additional_properties: + properties.update(additional_properties) + + return { + "type": "object", + "properties": properties, + "required": [ + DESCRIPTOR_MEDIATYPE_KEY, + DESCRIPTOR_SIZE_KEY, + DESCRIPTOR_DIGEST_KEY, + ] + + (additional_required or []), + } + +OCI_IMAGE_MANIFEST_CONTENT_TYPE = "application/vnd.oci.image.manifest.v1+json" +OCI_IMAGE_INDEX_CONTENT_TYPE = "application/vnd.oci.image.index.v1+json" + +ALLOWED_MEDIA_TYPES = [ + OCI_IMAGE_MANIFEST_CONTENT_TYPE, + OCI_IMAGE_INDEX_CONTENT_TYPE, +] +INDEX_VERSION_KEY = "schemaVersion" +INDEX_MEDIATYPE_KEY = "mediaType" +INDEX_SIZE_KEY = "size" +INDEX_DIGEST_KEY = "digest" +INDEX_URLS_KEY = "urls" +INDEX_MANIFESTS_KEY = "manifests" +INDEX_PLATFORM_KEY = "platform" +INDEX_ARCHITECTURE_KEY = "architecture" +INDEX_OS_KEY = "os" +INDEX_OS_VERSION_KEY = "os.version" +INDEX_OS_FEATURES_KEY = "os.features" +INDEX_FEATURES_KEY = "features" +INDEX_VARIANT_KEY = "variant" +INDEX_ANNOTATIONS_KEY = "annotations" + +class OCIIndex: + METASCHEMA = { + "type": "object", + "properties": { + INDEX_VERSION_KEY: { + "type": "number", + "description": "The version of the index. Must always be `2`.", + "minimum": 2, + "maximum": 2, + }, + INDEX_MEDIATYPE_KEY: { + "type": "string", + "description": "The media type of the index.", + "enum": [OCI_IMAGE_INDEX_CONTENT_TYPE], + }, + INDEX_MANIFESTS_KEY: { + "type": "array", + "description": "The manifests field contains a list of manifests for specific platforms", + "items": get_descriptor_schema( + allowed_media_types=ALLOWED_MEDIA_TYPES, + additional_properties={ + INDEX_PLATFORM_KEY: { + "type": "object", + "description": "The platform object describes the platform which the image in " + + "the manifest runs on", + "properties": { + INDEX_ARCHITECTURE_KEY: { + "type": "string", + "description": "Specifies the CPU architecture, for example amd64 or ppc64le.", + }, + INDEX_OS_KEY: { + "type": "string", + "description": "Specifies the operating system, for example linux or windows", + }, + INDEX_OS_VERSION_KEY: { + "type": "string", + "description": "Specifies the operating system version, for example 10.0.10586", + }, + INDEX_OS_FEATURES_KEY: { + "type": "array", + "description": "specifies an array of strings, each listing a required OS " + + "feature (for example on Windows win32k)", + "items": { + "type": "string", + }, + }, + INDEX_VARIANT_KEY: { + "type": "string", + "description": "Specifies a variant of the CPU, for example armv6l to specify " + + "a particular CPU variant of the ARM CPU", + }, + INDEX_FEATURES_KEY: { + "type": "array", + "description": "specifies an array of strings, each listing a required CPU " + + "feature (for example sse4 or aes).", + "items": { + "type": "string", + }, + }, + }, + "required": [ + INDEX_ARCHITECTURE_KEY, + INDEX_OS_KEY, + ], + }, + }, + additional_required=[INDEX_PLATFORM_KEY], + ), + }, + INDEX_ANNOTATIONS_KEY: { + "type": "object", + "description": "The annotations, if any, on this index", + "additionalProperties": True, + }, + }, + "required": [ + INDEX_VERSION_KEY, + INDEX_MANIFESTS_KEY, + ], + } + + + + + +OCI_IMAGE_CONFIG_CONTENT_TYPE = "application/vnd.oci.image.config.v1+json" + +OCI_IMAGE_MANIFEST_CONTENT_TYPE = "application/vnd.oci.image.manifest.v1+json" +OCI_IMAGE_INDEX_CONTENT_TYPE = "application/vnd.oci.image.index.v1+json" + +OCI_IMAGE_TAR_LAYER_CONTENT_TYPE = "application/vnd.oci.image.layer.v1.tar" +OCI_IMAGE_TAR_GZIP_LAYER_CONTENT_TYPE = "application/vnd.oci.image.layer.v1.tar+gzip" +OCI_IMAGE_TAR_ZSTD_LAYER_CONTENT_TYPE = "application/vnd.oci.image.layer.v1.tar+zstd" + +OCI_IMAGE_DISTRIBUTABLE_LAYER_CONTENT_TYPES = [ + OCI_IMAGE_TAR_LAYER_CONTENT_TYPE, + OCI_IMAGE_TAR_GZIP_LAYER_CONTENT_TYPE, + OCI_IMAGE_TAR_ZSTD_LAYER_CONTENT_TYPE, +] + +OCI_IMAGE_TAR_NON_DISTRIBUTABLE_LAYER_CONTENT_TYPE = ( + "application/vnd.oci.image.layer.nondistributable.v1.tar" +) +OCI_IMAGE_TAR_GZIP_NON_DISTRIBUTABLE_LAYER_CONTENT_TYPE = ( + "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" +) + +OCI_IMAGE_NON_DISTRIBUTABLE_LAYER_CONTENT_TYPES = [ + OCI_IMAGE_TAR_NON_DISTRIBUTABLE_LAYER_CONTENT_TYPE, + OCI_IMAGE_TAR_GZIP_NON_DISTRIBUTABLE_LAYER_CONTENT_TYPE, +] + +OCI_IMAGE_LAYER_CONTENT_TYPES = ( + OCI_IMAGE_DISTRIBUTABLE_LAYER_CONTENT_TYPES + OCI_IMAGE_NON_DISTRIBUTABLE_LAYER_CONTENT_TYPES +) +ALLOWED_ARTIFACT_TYPES = [OCI_IMAGE_CONFIG_CONTENT_TYPE] + +OCI_MANIFEST_VERSION_KEY = "schemaVersion" +OCI_MANIFEST_MEDIATYPE_KEY = "mediaType" +OCI_MANIFEST_CONFIG_KEY = "config" +OCI_MANIFEST_SIZE_KEY = "size" +OCI_MANIFEST_DIGEST_KEY = "digest" +OCI_MANIFEST_LAYERS_KEY = "layers" +OCI_MANIFEST_URLS_KEY = "urls" +OCI_MANIFEST_ANNOTATIONS_KEY = "annotations" +ADDITIONAL_LAYER_CONTENT_TYPES = [ + "application / vnd.oci.image.layer.v1.tar", + "application / vnd.oci.image.layer.v1.tar + gzip", + "application / vnd.oci.image.layer.nondistributable.v1.tar", + "application / vnd.oci.image.layer.nondistributable.v1.tar + gzip", +] + +# Named tuples. +OCIManifestConfig = namedtuple("OCIManifestConfig", ["size", "digest"]) +OCIManifestLayer = namedtuple( + "OCIManifestLayer", ["index", "digest", "is_remote", "urls", "compressed_size"] +) + +OCIManifestImageLayer = namedtuple( + "OCIManifestImageLayer", + ["history", "blob_layer", "v1_id", "v1_parent_id", "compressed_size", "blob_digest"], +) +class OCIManifest: + METASCHEMA = { + "type": "object", + "properties": { + OCI_MANIFEST_VERSION_KEY: { + "type": "number", + "description": "The version of the schema. Must always be `2`.", + "minimum": 2, + "maximum": 2, + }, + OCI_MANIFEST_MEDIATYPE_KEY: { + "type": "string", + "description": "The media type of the schema.", + "enum": [OCI_IMAGE_MANIFEST_CONTENT_TYPE], + }, + OCI_MANIFEST_CONFIG_KEY: get_descriptor_schema(ALLOWED_ARTIFACT_TYPES), + OCI_MANIFEST_LAYERS_KEY: { + "type": "array", + "description": "The array MUST have the base layer at index 0. Subsequent layers MUST then follow in stack order (i.e. from layers[0] to layers[len(layers)-1])", + "items": get_descriptor_schema( + OCI_IMAGE_LAYER_CONTENT_TYPES + ADDITIONAL_LAYER_CONTENT_TYPES + ), + }, + }, + "required": [ + OCI_MANIFEST_VERSION_KEY, + OCI_MANIFEST_CONFIG_KEY, + OCI_MANIFEST_LAYERS_KEY, + ], + } + + + +DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE = "application/vnd.docker.distribution.manifest.v2+json" +DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE = ( + "application/vnd.docker.distribution.manifest.list.v2+json" +) +DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE = "application/vnd.docker.distribution.manifest.v1+json" + + +DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY = "schemaVersion" +DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY = "mediaType" +DOCKER_SCHEMA2_MANIFESTLIST_SIZE_KEY = "size" +DOCKER_SCHEMA2_MANIFESTLIST_DIGEST_KEY = "digest" +DOCKER_SCHEMA2_MANIFESTLIST_MANIFESTS_KEY = "manifests" +DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY = "platform" +DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY = "architecture" +DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY = "os" +DOCKER_SCHEMA2_MANIFESTLIST_OS_VERSION_KEY = "os.version" +DOCKER_SCHEMA2_MANIFESTLIST_OS_FEATURES_KEY = "os.features" +DOCKER_SCHEMA2_MANIFESTLIST_FEATURES_KEY = "features" +DOCKER_SCHEMA2_MANIFESTLIST_VARIANT_KEY = "variant" + +class DockerSchema2ManifestList: + METASCHEMA = { + "type": "object", + "properties": { + DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY: { + "type": "number", + "description": "The version of the manifest list. Must always be `2`.", + "minimum": 2, + "maximum": 2, + }, + DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY: { + "type": "string", + "description": "The media type of the manifest list.", + "enum": [DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE], + }, + DOCKER_SCHEMA2_MANIFESTLIST_MANIFESTS_KEY: { + "type": "array", + "description": "The manifests field contains a list of manifests for specific platforms", + "items": { + "type": "object", + "properties": { + DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY: { + "type": "string", + "description": "The MIME type of the referenced object. This will generally be " + + "application/vnd.docker.distribution.manifest.v2+json, but it " + + "could also be application/vnd.docker.distribution.manifest.v1+json " + + "if the manifest list references a legacy schema-1 manifest.", + "enum": [ + DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE, + DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE, + ], + }, + DOCKER_SCHEMA2_MANIFESTLIST_SIZE_KEY: { + "type": "number", + "description": "The size in bytes of the object. This field exists so that a " + + "client will have an expected size for the content before " + + "validating. If the length of the retrieved content does not " + + "match the specified length, the content should not be trusted.", + }, + DOCKER_SCHEMA2_MANIFESTLIST_DIGEST_KEY: { + "type": "string", + "description": "The content addressable digest of the manifest in the blob store", + }, + DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY: { + "type": "object", + "description": "The platform object describes the platform which the image in " + + "the manifest runs on", + "properties": { + DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY: { + "type": "string", + "description": "Specifies the CPU architecture, for example amd64 or ppc64le.", + }, + DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY: { + "type": "string", + "description": "Specifies the operating system, for example linux or windows", + }, + DOCKER_SCHEMA2_MANIFESTLIST_OS_VERSION_KEY: { + "type": "string", + "description": "Specifies the operating system version, for example 10.0.10586", + }, + DOCKER_SCHEMA2_MANIFESTLIST_OS_FEATURES_KEY: { + "type": "array", + "description": "specifies an array of strings, each listing a required OS " + + "feature (for example on Windows win32k)", + "items": { + "type": "string", + }, + }, + DOCKER_SCHEMA2_MANIFESTLIST_VARIANT_KEY: { + "type": "string", + "description": "Specifies a variant of the CPU, for example armv6l to specify " + + "a particular CPU variant of the ARM CPU", + }, + DOCKER_SCHEMA2_MANIFESTLIST_FEATURES_KEY: { + "type": "array", + "description": "specifies an array of strings, each listing a required CPU " + + "feature (for example sse4 or aes).", + "items": { + "type": "string", + }, + }, + }, + "required": [ + DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY, + DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY, + ], + }, + }, + "required": [ + DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY, + DOCKER_SCHEMA2_MANIFESTLIST_SIZE_KEY, + DOCKER_SCHEMA2_MANIFESTLIST_DIGEST_KEY, + DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY, + ], + }, + }, + }, + "required": [ + DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY, + DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY, + DOCKER_SCHEMA2_MANIFESTLIST_MANIFESTS_KEY, + ], + } + + + + +DOCKER_SCHEMA2_CONFIG_CONTENT_TYPE = "application/vnd.docker.container.image.v1+json" +DOCKER_SCHEMA2_LAYER_CONTENT_TYPE = "application/vnd.docker.image.rootfs.diff.tar.gzip" +DOCKER_SCHEMA2_REMOTE_LAYER_CONTENT_TYPE = ( + "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" +) + +DOCKER_SCHEMA2_MANIFEST_VERSION_KEY = "schemaVersion" +DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY = "mediaType" +DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY = "config" +DOCKER_SCHEMA2_MANIFEST_SIZE_KEY = "size" +DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY = "digest" +DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY = "layers" +DOCKER_SCHEMA2_MANIFEST_URLS_KEY = "urls" + +class DockerSchema2Manifest: + METASCHEMA = { + "type": "object", + "properties": { + DOCKER_SCHEMA2_MANIFEST_VERSION_KEY: { + "type": "number", + "description": "The version of the schema. Must always be `2`.", + "minimum": 2, + "maximum": 2, + }, + DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY: { + "type": "string", + "description": "The media type of the schema.", + "enum": [DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE], + }, + DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY: { + "type": "object", + "description": "The config field references a configuration object for a container, " + + "by digest. This configuration item is a JSON blob that the runtime " + + "uses to set up the container.", + "properties": { + DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY: { + "type": "string", + "description": "The MIME type of the referenced object. This should generally be " + + "application/vnd.docker.container.image.v1+json", + "enum": [DOCKER_SCHEMA2_CONFIG_CONTENT_TYPE], + }, + DOCKER_SCHEMA2_MANIFEST_SIZE_KEY: { + "type": "number", + "description": "The size in bytes of the object. This field exists so that a " + + "client will have an expected size for the content before " + + "validating. If the length of the retrieved content does not " + + "match the specified length, the content should not be trusted.", + }, + DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY: { + "type": "string", + "description": "The content addressable digest of the config in the blob store", + }, + }, + "required": [ + DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY, + DOCKER_SCHEMA2_MANIFEST_SIZE_KEY, + DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY, + ], + }, + DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY: { + "type": "array", + "description": "The layer list is ordered starting from the base " + + "image (opposite order of schema1).", + "items": { + "type": "object", + "properties": { + DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY: { + "type": "string", + "description": "The MIME type of the referenced object. This should generally be " + + "application/vnd.docker.image.rootfs.diff.tar.gzip. Layers of type " + + "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be " + + "pulled from a remote location but they should never be pushed.", + "enum": [ + DOCKER_SCHEMA2_LAYER_CONTENT_TYPE, + DOCKER_SCHEMA2_REMOTE_LAYER_CONTENT_TYPE, + ], + }, + DOCKER_SCHEMA2_MANIFEST_SIZE_KEY: { + "type": "number", + "description": "The size in bytes of the object. This field exists so that a " + + "client will have an expected size for the content before " + + "validating. If the length of the retrieved content does not " + + "match the specified length, the content should not be trusted.", + }, + DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY: { + "type": "string", + "description": "The content addressable digest of the layer in the blob store", + }, + }, + "required": [ + DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY, + DOCKER_SCHEMA2_MANIFEST_SIZE_KEY, + DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY, + ], + }, + }, + }, + "required": [ + DOCKER_SCHEMA2_MANIFEST_VERSION_KEY, + DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY, + DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY, + DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY, + ], + } diff --git a/pulp_container/app/registry_api.py b/pulp_container/app/registry_api.py index 2054e8b7d..a063555c8 100644 --- a/pulp_container/app/registry_api.py +++ b/pulp_container/app/registry_api.py @@ -23,6 +23,7 @@ from django.shortcuts import get_object_or_404 from django.conf import settings +from jsonschema import validate, ValidationError as SchemaValidationError from pulpcore.plugin.models import Artifact, ContentArtifact, Task, UploadChunk from pulpcore.plugin.files import PulpTemporaryUploadedFile @@ -59,6 +60,12 @@ ManifestInvalid, ManifestSignatureInvalid, ) +from pulp_container.app.json_schemas import ( + OCIIndex, + OCIManifest, + DockerSchema2Manifest, + DockerSchema2ManifestList, +) from pulp_container.app.redirects import ( FileStorageRedirects, S3StorageRedirects, @@ -836,14 +843,6 @@ def put(self, request, path, pk=None): """ Responds with the actual manifest """ - # when a user uploads a manifest list with zero listed manifests (no blobs were uploaded - # before) and the specified repository has not been created yet, create the repository - # without raising an error - create_new_repo = request.content_type in ( - models.MEDIA_TYPE.MANIFEST_LIST, - models.MEDIA_TYPE.INDEX_OCI, - ) - _, repository = self.get_dr_push(request, path, create=create_new_repo) # iterate over all the layers and create chunk = request.META["wsgi.input"] artifact = self.receive_artifact(chunk) @@ -864,6 +863,20 @@ def put(self, request, path, pk=None): content_data = json.loads(raw_data) + try: + self.validate_content(content_data, request.content_type) + except SchemaValidationError: + raise ManifestInvalid(digest=manifest_digest) + + # when a user uploads a manifest list with zero listed manifests (no blobs were uploaded + # before) and the specified repository has not been created yet, create the repository + # without raising an error + create_new_repo = request.content_type in ( + models.MEDIA_TYPE.MANIFEST_LIST, + models.MEDIA_TYPE.INDEX_OCI, + ) + _, repository = self.get_dr_push(request, path, create=create_new_repo) + if request.content_type in ( models.MEDIA_TYPE.MANIFEST_LIST, models.MEDIA_TYPE.INDEX_OCI, @@ -980,6 +993,26 @@ def put(self, request, path, pk=None): if has_task_completed(dispatched_task): return ManifestResponse(manifest, path, request, status=201) + def validate_content(self, content_data, content_type): + try: + schema = self.init_schema(content_type) + except ValueError: + raise ValidationError() + else: + validate(content_data, schema) + + def init_schema(self, content_type): + if content_type == models.MEDIA_TYPE.MANIFEST_V2: + return DockerSchema2Manifest.METASCHEMA + elif content_type == models.MEDIA_TYPE.MANIFEST_OCI: + return OCIManifest.METASCHEMA + elif content_type == models.MEDIA_TYPE.MANIFEST_LIST: + return DockerSchema2ManifestList.METASCHEMA + elif content_type == models.MEDIA_TYPE.INDEX_OCI: + return OCIIndex.METASCHEMA + else: + raise ValueError() + def _save_manifest(self, artifact, manifest_digest, content_type, config_blob=None): manifest = models.Manifest( digest=manifest_digest,