From 4106f5ce51b99153d17520f8e0fc5225e103b0e7 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 10 Dec 2018 15:33:59 -0500 Subject: [PATCH] Fix manifest UI page to properly show the layers of manifests and show manifest lists --- data/registry_model/__init__.py | 3 + data/registry_model/datatypes.py | 9 +- data/registry_model/interface.py | 8 ++ data/registry_model/registry_oci_model.py | 12 +-- data/registry_model/registry_pre_oci_model.py | 8 +- endpoints/api/manifest.py | 36 ++++++-- ...view-layer.css => manifest-view-layer.css} | 22 ++--- static/directives/image-view-layer.html | 7 -- static/directives/manifest-view-layer.html | 7 ++ .../manifest-view-manifest-link.html | 1 + ...e-view-layer.js => manifest-view-layer.js} | 19 ++-- static/js/pages/manifest-view.js | 27 +++++- static/partials/manifest-view.html | 92 +++++++++++-------- 13 files changed, 162 insertions(+), 89 deletions(-) rename static/css/directives/ui/{image-view-layer.css => manifest-view-layer.css} (55%) delete mode 100644 static/directives/image-view-layer.html create mode 100644 static/directives/manifest-view-layer.html create mode 100644 static/directives/manifest-view-manifest-link.html rename static/js/directives/ui/{image-view-layer.js => manifest-view-layer.js} (50%) 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..e4cfacef1 100644 --- a/data/registry_model/interface.py +++ b/data/registry_model/interface.py @@ -194,6 +194,14 @@ 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`. + """ + @abstractmethod def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, storage, include_placements=False): 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 04ba3a117..03d44e0cb 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/endpoints/api/manifest.py b/endpoints/api/manifest.py index b8fe7d61f..3183796d5 100644 --- a/endpoints/api/manifest.py +++ b/endpoints/api/manifest.py @@ -1,14 +1,16 @@ """ Manage the manifests of a repository. """ +import json + 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 @@ -27,15 +29,37 @@ def _label_dict(label): 'media_type': label.media_type_name, } -def _manifest_dict(manifest): - image = None - if manifest.legacy_image is not None: - image = image_dict(manifest.legacy_image, with_history=True) + +def _layer_dict(manifest_layer, index): + try: + command = json.loads(manifest_layer.command) + except (TypeError, ValueError): + command = [] + return { + 'index': index, + 'compressed_size': manifest_layer.compressed_size, + 'is_remote': manifest_layer.is_remote, + 'urls': manifest_layer.urls, + 'command': command, + 'blob_digest': str(manifest_layer.blob_digest), + 'created_datetime': format_date(manifest_layer.created_datetime), + } + + +def _manifest_dict(manifest): + image = None + if manifest.legacy_image_if_present is not None: + image = image_dict(manifest.legacy_image, with_history=True) + + layers = registry_model.list_manifest_layers(manifest, storage) return { 'digest': manifest.digest, + 'is_manifest_list': manifest.is_manifest_list, 'manifest_data': manifest.manifest_bytes, 'image': image, + 'layers': ([_layer_dict(lyr.layer_info, idx) for idx, lyr in enumerate(layers)] + if layers else None), } diff --git a/static/css/directives/ui/image-view-layer.css b/static/css/directives/ui/manifest-view-layer.css similarity index 55% rename from static/css/directives/ui/image-view-layer.css rename to static/css/directives/ui/manifest-view-layer.css index 4c1853520..8e2704011 100644 --- a/static/css/directives/ui/image-view-layer.css +++ b/static/css/directives/ui/manifest-view-layer.css @@ -1,19 +1,19 @@ -.image-view-layer-element { +.manifest-view-layer-element { position: relative; padding: 10px; padding-left: 40px; } -.image-view-layer-element .image-comment { +.manifest-view-layer-element .image-comment { margin-bottom: 10px; } -.image-view-layer-element .nondocker-command { +.manifest-view-layer-element .nondocker-command { font-family: monospace; padding: 2px; } -.image-view-layer-element .nondocker-command:before { +.manifest-view-layer-element .nondocker-command:before { content: "\f120"; font-family: "FontAwesome"; font-size: 16px; @@ -21,7 +21,7 @@ color: #999; } -.image-view-layer-element .image-layer-line { +.manifest-view-layer-element .image-layer-line { position: absolute; top: 0px; bottom: 0px; @@ -31,15 +31,15 @@ width: 0px; } -.image-view-layer-element.first .image-layer-line { +.manifest-view-layer-element.first .image-layer-line { top: 20px; } -.image-view-layer-element.last .image-layer-line { +.manifest-view-layer-element.last .image-layer-line { height: 16px; } -.image-view-layer-element .image-layer-dot { +.manifest-view-layer-element .image-layer-dot { position: absolute; top: 14px; left: 5px; @@ -52,17 +52,17 @@ z-index: 2; } -.image-view-layer-element.first .image-layer-dot { +.manifest-view-layer-element.first .image-layer-dot { background: #428bca; } @media (max-width: 767px) { - .image-view-layer-element .dockerfile-command-element .label { + .manifest-view-layer-element .dockerfile-command-element .label { position: relative; display: block; } - .image-view-layer-element .dockerfile-command-element .command-title { + .manifest-view-layer-element .dockerfile-command-element .command-title { padding-top: 10px; padding-left: 0px; } diff --git a/static/directives/image-view-layer.html b/static/directives/image-view-layer.html deleted file mode 100644 index 47376f320..000000000 --- a/static/directives/image-view-layer.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
- -
-
-
-
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/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..f4e612507 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

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