Show manifest digests in place of V1 ids in the tag view when possible
This commit is contained in:
parent
814bbb4a96
commit
af743b156b
8 changed files with 88 additions and 11 deletions
|
@ -82,6 +82,16 @@ def filter_tags_have_repository_event(query, event):
|
||||||
.where(RepositoryNotification.event == event)
|
.where(RepositoryNotification.event == event)
|
||||||
.order_by(RepositoryTag.lifetime_start_ts.desc()))
|
.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,
|
def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
||||||
include_storage=False):
|
include_storage=False):
|
||||||
to_select = (RepositoryTag, Image)
|
to_select = (RepositoryTag, Image)
|
||||||
|
@ -296,6 +306,7 @@ def list_repository_tag_history(repo_obj, page=1, size=100, specific_tag=None):
|
||||||
query = (RepositoryTag
|
query = (RepositoryTag
|
||||||
.select(RepositoryTag, Image)
|
.select(RepositoryTag, Image)
|
||||||
.join(Image)
|
.join(Image)
|
||||||
|
.switch(RepositoryTag)
|
||||||
.where(RepositoryTag.repository == repo_obj)
|
.where(RepositoryTag.repository == repo_obj)
|
||||||
.where(RepositoryTag.hidden == False)
|
.where(RepositoryTag.hidden == False)
|
||||||
.order_by(RepositoryTag.lifetime_start_ts.desc(), RepositoryTag.name)
|
.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)
|
query = query.where(RepositoryTag.name == specific_tag)
|
||||||
|
|
||||||
tags = list(query)
|
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):
|
def revert_tag(repo_obj, tag_name, docker_image_id):
|
||||||
|
|
|
@ -259,7 +259,7 @@ class Repository(RepositoryParamResource):
|
||||||
"""Fetch the specified repository."""
|
"""Fetch the specified repository."""
|
||||||
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
||||||
|
|
||||||
def tag_view(tag):
|
def tag_view(tag, manifest):
|
||||||
tag_info = {
|
tag_info = {
|
||||||
'name': tag.name,
|
'name': tag.name,
|
||||||
'image_id': tag.image.docker_image_id,
|
'image_id': tag.image.docker_image_id,
|
||||||
|
@ -270,13 +270,18 @@ class Repository(RepositoryParamResource):
|
||||||
last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts))
|
last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts))
|
||||||
tag_info['last_modified'] = last_modified
|
tag_info['last_modified'] = last_modified
|
||||||
|
|
||||||
|
if manifest is not None:
|
||||||
|
tag_info['manifest_digest'] = manifest.digest
|
||||||
|
|
||||||
return tag_info
|
return tag_info
|
||||||
|
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace, repository)
|
||||||
stats = None
|
stats = None
|
||||||
if repo:
|
if repo:
|
||||||
tags = model.tag.list_repository_tags(namespace, repository, include_storage=True)
|
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_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||||
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
||||||
|
|
||||||
|
|
|
@ -41,19 +41,22 @@ class ListRepositoryTags(RepositoryParamResource):
|
||||||
if tag.lifetime_end_ts > 0:
|
if tag.lifetime_end_ts > 0:
|
||||||
tag_info['end_ts'] = tag.lifetime_end_ts
|
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
|
return tag_info
|
||||||
|
|
||||||
specific_tag = parsed_args.get('specificTag') or None
|
specific_tag = parsed_args.get('specificTag') or None
|
||||||
|
|
||||||
page = max(1, parsed_args.get('page', 1))
|
page = max(1, parsed_args.get('page', 1))
|
||||||
limit = min(100, max(1, parsed_args.get('limit', 50)))
|
limit = min(100, max(1, parsed_args.get('limit', 50)))
|
||||||
tags, has_additional = model.tag.list_repository_tag_history(repo, page=page, size=limit,
|
tags, manifest_map, more = model.tag.list_repository_tag_history(repo, page=page, size=limit,
|
||||||
specific_tag=specific_tag)
|
specific_tag=specific_tag)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'tags': [tag_view(tag) for tag in tags],
|
'tags': [tag_view(tag) for tag in tags],
|
||||||
'page': page,
|
'page': page,
|
||||||
'has_additional': has_additional,
|
'has_additional': more,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,28 @@
|
||||||
|
.image-link {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
.image-link a {
|
.image-link a {
|
||||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
font-size: 12px;
|
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;
|
||||||
}
|
}
|
|
@ -1,2 +1,17 @@
|
||||||
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ imageId }}"
|
<span>
|
||||||
class="image-link-element" bindonce>{{ imageId.substr(0, 12) }}</a>
|
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ imageId }}"
|
||||||
|
class="image-link-element" bindonce>
|
||||||
|
<span class="id-label" ng-if="!hasSHA256(manifestDigest)"
|
||||||
|
data-title="The Docker V1 ID for this image. This ID is not content addressable nor is it stable across pulls."
|
||||||
|
data-container="body"
|
||||||
|
bs-tooltip>V1ID</span>
|
||||||
|
|
||||||
|
<span class="id-label cas" ng-if="hasSHA256(manifestDigest)"
|
||||||
|
data-title="The content-addressable SHA256 hash of this layer."
|
||||||
|
data-container="body"
|
||||||
|
bs-tooltip>SHA256</span>
|
||||||
|
|
||||||
|
<span ng-if="!hasSHA256(manifestDigest)">{{ imageId.substr(0, 12) }}</span>
|
||||||
|
<span ng-if="hasSHA256(manifestDigest)">{{ getShortDigest(manifestDigest) }}</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm"
|
<td class="hidden-xs hidden-sm"
|
||||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||||
style="width: 120px;">
|
style="width: 140px;">
|
||||||
<a ng-click="orderBy('image_id')">Image</a>
|
<a ng-click="orderBy('image_id')">Image</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
<td class="hidden-xs hidden-sm image-track" ng-repeat="it in imageTracks"
|
||||||
|
@ -200,7 +200,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-sm hidden-xs" bo-text="tag.size | bytes"></td>
|
<td class="hidden-sm hidden-xs" bo-text="tag.size | bytes"></td>
|
||||||
<td class="hidden-xs hidden-sm image-id-col">
|
<td class="hidden-xs hidden-sm image-id-col">
|
||||||
<span class="image-link" repository="repository" image-id="tag.image_id"></span>
|
<span class="image-link" repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs hidden-sm image-track"
|
<td class="hidden-xs hidden-sm image-track"
|
||||||
ng-if="imageTracks.length > maxTrackCount" bindonce>
|
ng-if="imageTracks.length > maxTrackCount" bindonce>
|
||||||
|
|
|
@ -10,9 +10,17 @@ angular.module('quay').directive('imageLink', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'imageId': '=imageId'
|
'imageId': '=imageId',
|
||||||
|
'manifestDigest': '=?manifestDigest'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
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;
|
return directiveDefinitionObject;
|
||||||
|
|
|
@ -3030,6 +3030,13 @@ class TestListAndDeleteTag(ApiTestCase):
|
||||||
|
|
||||||
self.assertEquals(prod_images, json['images'])
|
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):
|
def test_listtagpagination(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
|
Reference in a new issue