diff --git a/data/model/tag.py b/data/model/tag.py index 8f75b1650..928727176 100644 --- a/data/model/tag.py +++ b/data/model/tag.py @@ -82,6 +82,16 @@ def filter_tags_have_repository_event(query, event): .where(RepositoryNotification.event == event) .order_by(RepositoryTag.lifetime_start_ts.desc())) + +def get_tag_manifests(tags): + """ Returns a map from tag ID to its associated manifest, if any. """ + if not tags: + return dict() + + manifests = TagManifest.select().where(TagManifest.tag << [t.id for t in tags]) + return {manifest.tag_id:manifest for manifest in manifests} + + def list_repository_tags(namespace_name, repository_name, include_hidden=False, include_storage=False): to_select = (RepositoryTag, Image) @@ -296,6 +306,7 @@ def list_repository_tag_history(repo_obj, page=1, size=100, specific_tag=None): query = (RepositoryTag .select(RepositoryTag, Image) .join(Image) + .switch(RepositoryTag) .where(RepositoryTag.repository == repo_obj) .where(RepositoryTag.hidden == False) .order_by(RepositoryTag.lifetime_start_ts.desc(), RepositoryTag.name) @@ -306,7 +317,11 @@ def list_repository_tag_history(repo_obj, page=1, size=100, specific_tag=None): query = query.where(RepositoryTag.name == specific_tag) tags = list(query) - return tags[0:size], len(tags) > size + if not tags: + return [], {}, False + + manifest_map = get_tag_manifests(tags) + return tags[0:size], manifest_map, len(tags) > size def revert_tag(repo_obj, tag_name, docker_image_id): diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 2f8baed5b..d8b08483b 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -259,7 +259,7 @@ class Repository(RepositoryParamResource): """Fetch the specified repository.""" logger.debug('Get repo: %s/%s' % (namespace, repository)) - def tag_view(tag): + def tag_view(tag, manifest): tag_info = { 'name': tag.name, 'image_id': tag.image.docker_image_id, @@ -270,13 +270,18 @@ class Repository(RepositoryParamResource): last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts)) tag_info['last_modified'] = last_modified + if manifest is not None: + tag_info['manifest_digest'] = manifest.digest + return tag_info repo = model.repository.get_repository(namespace, repository) stats = None if repo: tags = model.tag.list_repository_tags(namespace, repository, include_storage=True) - tag_dict = {tag.name: tag_view(tag) for tag in tags} + manifests = model.tag.get_tag_manifests(tags) + + tag_dict = {tag.name: tag_view(tag, manifests.get(tag.id)) for tag in tags} can_write = ModifyRepositoryPermission(namespace, repository).can() can_admin = AdministerRepositoryPermission(namespace, repository).can() diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 3a24200b8..a587186d2 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -41,19 +41,22 @@ class ListRepositoryTags(RepositoryParamResource): if tag.lifetime_end_ts > 0: tag_info['end_ts'] = tag.lifetime_end_ts + if tag.id in manifest_map: + tag_info['manifest_digest'] = manifest_map[tag.id].digest + return tag_info specific_tag = parsed_args.get('specificTag') or None page = max(1, parsed_args.get('page', 1)) limit = min(100, max(1, parsed_args.get('limit', 50))) - tags, has_additional = model.tag.list_repository_tag_history(repo, page=page, size=limit, - specific_tag=specific_tag) + tags, manifest_map, more = model.tag.list_repository_tag_history(repo, page=page, size=limit, + specific_tag=specific_tag) return { 'tags': [tag_view(tag) for tag in tags], 'page': page, - 'has_additional': has_additional, + 'has_additional': more, } diff --git a/static/css/directives/ui/image-link.css b/static/css/directives/ui/image-link.css index 03e39f3d4..f7dc88382 100644 --- a/static/css/directives/ui/image-link.css +++ b/static/css/directives/ui/image-link.css @@ -1,4 +1,28 @@ +.image-link { + display: inline-block; + white-space: nowrap; + width: 130px; +} + .image-link a { font-family: Consolas, "Lucida Console", Monaco, monospace; font-size: 12px; + text-decoration: none; +} + +.image-link .id-label { + font-size: 10px; + cursor: pointer; + padding: 2px; + background-color: #eee; + border-radius: 5px; + padding-left: 4px; + padding-right: 4px; + width: 40px; + text-align: center; + color: black !important; +} + +.image-link .id-label.cas { + background-color: #e8f1f6; } \ No newline at end of file diff --git a/static/directives/image-link.html b/static/directives/image-link.html index 65e116f76..af4e173c1 100644 --- a/static/directives/image-link.html +++ b/static/directives/image-link.html @@ -1,2 +1,17 @@ -{{ imageId.substr(0, 12) }} + + + V1ID + + SHA256 + + {{ imageId.substr(0, 12) }} + {{ getShortDigest(manifestDigest) }} + + diff --git a/static/directives/repo-view/repo-panel-tags.html b/static/directives/repo-view/repo-panel-tags.html index 67a49f043..5907a0b6d 100644 --- a/static/directives/repo-view/repo-panel-tags.html +++ b/static/directives/repo-view/repo-panel-tags.html @@ -101,7 +101,7 @@ + style="width: 140px;"> Image - + diff --git a/static/js/directives/ui/image-link.js b/static/js/directives/ui/image-link.js index 0752965c2..7a3bcf2c4 100644 --- a/static/js/directives/ui/image-link.js +++ b/static/js/directives/ui/image-link.js @@ -10,9 +10,17 @@ angular.module('quay').directive('imageLink', function () { restrict: 'C', scope: { 'repository': '=repository', - 'imageId': '=imageId' + 'imageId': '=imageId', + 'manifestDigest': '=?manifestDigest' }, controller: function($scope, $element) { + $scope.hasSHA256 = function(digest) { + return digest && digest.indexOf('sha256:') == 0; + }; + + $scope.getShortDigest = function(digest) { + return digest.substr('sha256:'.length).substr(0, 12); + }; } }; return directiveDefinitionObject; diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 2501faf54..bed01fd46 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -3030,6 +3030,13 @@ class TestListAndDeleteTag(ApiTestCase): self.assertEquals(prod_images, json['images']) + def test_listtag_digest(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse(ListRepositoryTags, + params=dict(repository=ADMIN_ACCESS_USER + '/simple', page=1, + limit=1)) + self.assertTrue('manifest_digest' in json['tags'][0]) + def test_listtagpagination(self): self.login(ADMIN_ACCESS_USER)