diff --git a/data/registry_model/__init__.py b/data/registry_model/__init__.py index 79b20ff61..f806bd4c3 100644 --- a/data/registry_model/__init__.py +++ b/data/registry_model/__init__.py @@ -13,6 +13,9 @@ class RegistryModelProxy(object): self._model = oci_model if os.getenv('OCI_DATA_MODEL') == 'true' else pre_oci_model def setup_split(self, v22_whitelist): + if os.getenv('OCI_DATA_MODEL') == 'true': + return + logger.info('===============================') logger.info('Enabling split registry model with namespace whitelist `%s`', v22_whitelist) logger.info('===============================') diff --git a/data/registry_model/datatypes.py b/data/registry_model/datatypes.py index 4cfc69185..172905647 100644 --- a/data/registry_model/datatypes.py +++ b/data/registry_model/datatypes.py @@ -207,7 +207,14 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes'])) @property @requiresinput('legacy_image') def legacy_image(self, legacy_image): - """ Returns the legacy Docker V1-style image for this manifest. Note that this + """ Returns the legacy Docker V1-style image for this manifest. + """ + return legacy_image + + @property + @optionalinput('legacy_image') + def legacy_image_if_present(self, legacy_image): + """ Returns the legacy Docker V1-style image for this manifest. Note that this will be None for manifests that point to other manifests instead of images. """ return legacy_image diff --git a/data/registry_model/interface.py b/data/registry_model/interface.py index e946554ce..060194c1c 100644 --- a/data/registry_model/interface.py +++ b/data/registry_model/interface.py @@ -194,13 +194,21 @@ class RegistryDataInterface(object): def get_manifest_local_blobs(self, manifest, include_placements=False): """ Returns the set of local blobs for the given manifest or None if none. """ + @abstractmethod + def list_manifest_layers(self, manifest, storage, include_placements=False): + """ Returns an *ordered list* of the layers found in the manifest, starting at the base + and working towards the leaf, including the associated Blob and its placements + (if specified). The layer information in `layer_info` will be of type + `image.docker.types.ManifestImageLayer`. Should not be called for a manifest list. + """ + @abstractmethod def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, storage, include_placements=False): """ Returns an *ordered list* of the layers found in the parsed manifest, starting at the base and working towards the leaf, including the associated Blob and its placements (if specified). The layer information in `layer_info` will be of type - `image.docker.types.ManifestImageLayer`. + `image.docker.types.ManifestImageLayer`. Should not be called for a manifest list. """ @abstractmethod diff --git a/data/registry_model/registry_oci_model.py b/data/registry_model/registry_oci_model.py index 3b62dbe2d..1493f942a 100644 --- a/data/registry_model/registry_oci_model.py +++ b/data/registry_model/registry_oci_model.py @@ -115,7 +115,7 @@ class OCIModel(SharedModel, RegistryDataInterface): legacy_image_id = database.ManifestLegacyImage.get(manifest=manifest).image.docker_image_id legacy_image = self.get_legacy_image(repository_ref, legacy_image_id, include_parents=True) except database.ManifestLegacyImage.DoesNotExist: - return None + pass return Manifest.for_manifest(manifest, legacy_image) @@ -414,11 +414,7 @@ class OCIModel(SharedModel, RegistryDataInterface): legacy_image = oci.shared.get_legacy_image_for_manifest(manifest) return Manifest.for_manifest(manifest, LegacyImage.for_image(legacy_image)) - def list_manifest_layers(self, manifest, include_placements=False): - """ Returns an *ordered list* of the layers found in the manifest, starting at the base and - working towards the leaf, including the associated Blob and its placements (if specified). - Returns None if the manifest could not be parsed and validated. - """ + def list_manifest_layers(self, manifest, storage, include_placements=False): try: manifest_obj = database.Manifest.get(id=manifest._db_id) except database.Manifest.DoesNotExist: @@ -431,8 +427,8 @@ class OCIModel(SharedModel, RegistryDataInterface): logger.exception('Could not parse and validate manifest `%s`', manifest._db_id) return None - return self._list_manifest_layers(manifest_obj.repository_id, parsed, include_placements, - by_manifest=True) + return self._list_manifest_layers(manifest_obj.repository_id, parsed, storage, + include_placements, by_manifest=True) def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False): """ diff --git a/data/registry_model/registry_pre_oci_model.py b/data/registry_model/registry_pre_oci_model.py index 8f0aa377d..e8b6ca995 100644 --- a/data/registry_model/registry_pre_oci_model.py +++ b/data/registry_model/registry_pre_oci_model.py @@ -479,11 +479,7 @@ class PreOCIModel(SharedModel, RegistryDataInterface): return Manifest.for_tag_manifest(tag_manifest) - def list_manifest_layers(self, manifest, include_placements=False): - """ Returns an *ordered list* of the layers found in the manifest, starting at the base and - working towards the leaf, including the associated Blob and its placements (if specified). - Returns None if the manifest could not be parsed and validated. - """ + def list_manifest_layers(self, manifest, storage, include_placements=False): try: tag_manifest = database.TagManifest.get(id=manifest._db_id) except database.TagManifest.DoesNotExist: @@ -497,7 +493,7 @@ class PreOCIModel(SharedModel, RegistryDataInterface): return None repo_ref = RepositoryReference.for_id(tag_manifest.tag.repository_id) - return self.list_parsed_manifest_layers(repo_ref, parsed, include_placements) + return self.list_parsed_manifest_layers(repo_ref, parsed, storage, include_placements) def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False): """ diff --git a/data/registry_model/shared.py b/data/registry_model/shared.py index 8b62d86fd..16c87b96c 100644 --- a/data/registry_model/shared.py +++ b/data/registry_model/shared.py @@ -12,6 +12,7 @@ from data.registry_model.datatype import FromDictionaryException from data.registry_model.datatypes import (RepositoryReference, Blob, TorrentInfo, BlobUpload, LegacyImage, ManifestLayer, DerivedImage) from image.docker.schema1 import ManifestException, DockerSchema1ManifestBuilder +from image.docker.schema2 import EMPTY_LAYER_BLOB_DIGEST logger = logging.getLogger(__name__) @@ -344,14 +345,23 @@ class SharedModel: working towards the leaf, including the associated Blob and its placements (if specified). Returns None if the manifest could not be parsed and validated. """ + assert not parsed.is_manifest_list + + retriever = RepositoryContentRetriever(repo_id, storage) + requires_empty_blob = parsed.get_requires_empty_layer_blob(retriever) + storage_map = {} - if parsed.local_blob_digests: + blob_digests = list(parsed.local_blob_digests) + if requires_empty_blob: + blob_digests.append(EMPTY_LAYER_BLOB_DIGEST) + + if blob_digests: blob_query = model.storage.lookup_repo_storages_by_content_checksum(repo_id, - parsed.local_blob_digests, + blob_digests, by_manifest=by_manifest) storage_map = {blob.content_checksum: blob for blob in blob_query} - retriever = RepositoryContentRetriever(repo_id, storage) + layers = parsed.get_layers(retriever) if layers is None: logger.error('Could not load layers for manifest `%s`', parsed.digest) diff --git a/endpoints/api/manifest.py b/endpoints/api/manifest.py index b8fe7d61f..fa2d54e0d 100644 --- a/endpoints/api/manifest.py +++ b/endpoints/api/manifest.py @@ -1,14 +1,17 @@ """ Manage the manifests of a repository. """ +import json +import logging + from flask import request -from app import label_validator +from app import label_validator, storage from data.model import InvalidLabelKeyException, InvalidMediaTypeException from data.registry_model import registry_model from digest import digest_tools from endpoints.api import (resource, nickname, require_repo_read, require_repo_write, RepositoryParamResource, log_action, validate_json_request, path_param, parse_args, query_param, abort, api, - disallow_for_app_repositories) + disallow_for_app_repositories, format_date) from endpoints.api.image import image_dict from endpoints.exception import NotFound from util.validation import VALID_LABEL_KEY_REGEX @@ -18,6 +21,9 @@ BASE_MANIFEST_ROUTE = '/v1/repository//manifest/ -
- -
-
-
- diff --git a/static/directives/manifest-view-layer.html b/static/directives/manifest-view-layer.html new file mode 100644 index 000000000..b58005250 --- /dev/null +++ b/static/directives/manifest-view-layer.html @@ -0,0 +1,7 @@ +
+
+ +
+
+
+
diff --git a/static/directives/manifest-view-manifest-link.html b/static/directives/manifest-view-manifest-link.html new file mode 100644 index 000000000..68bf3c2ab --- /dev/null +++ b/static/directives/manifest-view-manifest-link.html @@ -0,0 +1 @@ + diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html index 36dba2047..5f32f4cb9 100644 --- a/static/directives/repo-view/repo-panel-tags.html +++ b/static/directives/repo-view/repo-panel-tags.html @@ -298,7 +298,7 @@ - +
diff --git a/static/js/directives/ui/image-view-layer.js b/static/js/directives/ui/manifest-view-layer.js similarity index 50% rename from static/js/directives/ui/image-view-layer.js rename to static/js/directives/ui/manifest-view-layer.js index 731e42284..b7d1e4d4e 100644 --- a/static/js/directives/ui/image-view-layer.js +++ b/static/js/directives/ui/manifest-view-layer.js @@ -1,27 +1,26 @@ /** - * An element which displays a single layer representing an image in the image view. + * An element which displays a single layer in the manifest view. */ -angular.module('quay').directive('imageViewLayer', function () { +angular.module('quay').directive('manifestViewLayer', function () { var directiveDefinitionObject = { priority: 0, - templateUrl: '/static/directives/image-view-layer.html', + templateUrl: '/static/directives/manifest-view-layer.html', replace: false, transclude: true, restrict: 'C', scope: { 'repository': '=repository', - 'image': '=image', - 'images': '=images' + 'manifest': '=manifest', + 'layer': '=layer' }, controller: function($scope, $element) { $scope.getClass = function() { - var index = $.inArray($scope.image, $scope.images); - if (index < 0) { - return 'first'; + if ($scope.layer.index == 0) { + return 'last'; } - if (index == $scope.images.length - 1) { - return 'last'; + if ($scope.layer.index == $scope.manifest.layers.length - 1) { + return 'first'; } return ''; diff --git a/static/js/pages/manifest-view.js b/static/js/pages/manifest-view.js index bbb4506cc..70ec57231 100644 --- a/static/js/pages/manifest-view.js +++ b/static/js/pages/manifest-view.js @@ -30,7 +30,7 @@ $scope.manifestResource = ApiService.getRepoManifestAsResource(params).get(function(manifest) { $scope.manifest = manifest; - $scope.reversedHistory = manifest.image.history.reverse(); + $scope.reversedLayers = manifest.layers ? manifest.layers.reverse() : null; }); }; @@ -57,5 +57,30 @@ if (!Features.SECURITY_SCANNER) { return; } $scope.manifestPackageCounter++; }; + + $scope.manifestsOf = function(manifest) { + if (!manifest || !manifest.is_manifest_list) { + return []; + } + + if (!manifest._mapped_manifests) { + // Calculate once and cache to avoid angular digest cycles. + var parsed_manifest = JSON.parse(manifest.manifest_data); + + manifest._mapped_manifests = parsed_manifest.manifests.map(function(manifest) { + return { + 'repository': $scope.repository, + 'raw': manifest, + 'os': manifest.platform.os, + 'architecture': manifest.platform.architecture, + 'size': manifest.size, + 'digest': manifest.digest, + 'description': `${manifest.platform.os} on ${manifest.platform.architecture}`, + }; + }); + } + + return manifest._mapped_manifests; + }; } })(); diff --git a/static/partials/manifest-view.html b/static/partials/manifest-view.html index 2643a671d..7c0539a19 100644 --- a/static/partials/manifest-view.html +++ b/static/partials/manifest-view.html @@ -10,51 +10,65 @@ - + {{ manifest.digest.substr(7, 12) }}
- - - - - - - - - - - - + +
+
+ + + + + +
+
- - - -

Manifest Layers

-
-
-
+ +
+ + + + + + + + + + + + - - -
-
-
-
+ + + +

Manifest Layers

+
+
- - -
-
-
-
-
-
+ + +
+
+
+
+ + + +
+
+
+
+ + +