Change revert tag into restore tag and add manifest support
This commit is contained in:
parent
35752176b5
commit
e90cab4d77
4 changed files with 137 additions and 56 deletions
|
@ -324,32 +324,69 @@ def list_repository_tag_history(repo_obj, page=1, size=100, specific_tag=None):
|
||||||
return tags[0:size], manifest_map, len(tags) > size
|
return tags[0:size], manifest_map, len(tags) > size
|
||||||
|
|
||||||
|
|
||||||
def revert_tag(repo_obj, tag_name, docker_image_id):
|
def restore_tag_to_manifest(repo_obj, tag_name, manifest_digest):
|
||||||
""" Reverts a tag to a specific image ID. """
|
""" Restores a tag to a specific manifest digest. """
|
||||||
# Verify that the image ID already existed under this repository under the
|
with db_transaction():
|
||||||
# tag.
|
# Verify that the manifest digest already existed under this repository under the
|
||||||
try:
|
# tag.
|
||||||
(RepositoryTag
|
try:
|
||||||
.select()
|
manifest = (TagManifest
|
||||||
.join(Image)
|
.select(TagManifest, RepositoryTag, Image)
|
||||||
.where(RepositoryTag.repository == repo_obj)
|
.join(RepositoryTag)
|
||||||
.where(RepositoryTag.name == tag_name)
|
.join(Image)
|
||||||
.where(Image.docker_image_id == docker_image_id)
|
.where(RepositoryTag.repository == repo_obj)
|
||||||
.get())
|
.where(RepositoryTag.name == tag_name)
|
||||||
except RepositoryTag.DoesNotExist:
|
.where(TagManifest.digest == manifest_digest)
|
||||||
raise DataModelException('Cannot revert to unknown or invalid image')
|
.get())
|
||||||
|
except TagManifest.DoesNotExist:
|
||||||
|
raise DataModelException('Cannot restore to unknown or invalid digest')
|
||||||
|
|
||||||
return create_or_update_tag(repo_obj.namespace_user.username, repo_obj.name, tag_name,
|
# Lookup the existing image, if any.
|
||||||
docker_image_id, reversion=True)
|
try:
|
||||||
|
existing_image = get_repo_tag_image(repo_obj, tag_name)
|
||||||
|
except DataModelException:
|
||||||
|
existing_image = None
|
||||||
|
|
||||||
|
docker_image_id = manifest.tag.image.docker_image_id
|
||||||
|
store_tag_manifest(repo_obj.namespace_user.username, repo_obj.name, tag_name, docker_image_id,
|
||||||
|
manifest_digest, manifest.json_data, reversion=True)
|
||||||
|
return existing_image
|
||||||
|
|
||||||
|
|
||||||
|
def restore_tag_to_image(repo_obj, tag_name, docker_image_id):
|
||||||
|
""" Restores a tag to a specific image ID. """
|
||||||
|
with db_transaction():
|
||||||
|
# Verify that the image ID already existed under this repository under the
|
||||||
|
# tag.
|
||||||
|
try:
|
||||||
|
(RepositoryTag
|
||||||
|
.select()
|
||||||
|
.join(Image)
|
||||||
|
.where(RepositoryTag.repository == repo_obj)
|
||||||
|
.where(RepositoryTag.name == tag_name)
|
||||||
|
.where(Image.docker_image_id == docker_image_id)
|
||||||
|
.get())
|
||||||
|
except RepositoryTag.DoesNotExist:
|
||||||
|
raise DataModelException('Cannot restore to unknown or invalid image')
|
||||||
|
|
||||||
|
# Lookup the existing image, if any.
|
||||||
|
try:
|
||||||
|
existing_image = get_repo_tag_image(repo_obj, tag_name)
|
||||||
|
except DataModelException:
|
||||||
|
existing_image = None
|
||||||
|
|
||||||
|
create_or_update_tag(repo_obj.namespace_user.username, repo_obj.name, tag_name,
|
||||||
|
docker_image_id, reversion=True)
|
||||||
|
return existing_image
|
||||||
|
|
||||||
|
|
||||||
def store_tag_manifest(namespace, repo_name, tag_name, docker_image_id, manifest_digest,
|
def store_tag_manifest(namespace, repo_name, tag_name, docker_image_id, manifest_digest,
|
||||||
manifest_data):
|
manifest_data, reversion=False):
|
||||||
""" Stores a tag manifest for a specific tag name in the database. Returns the TagManifest
|
""" Stores a tag manifest for a specific tag name in the database. Returns the TagManifest
|
||||||
object, as well as a boolean indicating whether the TagManifest was created.
|
object, as well as a boolean indicating whether the TagManifest was created.
|
||||||
"""
|
"""
|
||||||
with db_transaction():
|
with db_transaction():
|
||||||
tag = create_or_update_tag(namespace, repo_name, tag_name, docker_image_id)
|
tag = create_or_update_tag(namespace, repo_name, tag_name, docker_image_id, reversion=reversion)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manifest = TagManifest.get(digest=manifest_digest)
|
manifest = TagManifest.get(digest=manifest_digest)
|
||||||
|
|
|
@ -180,15 +180,15 @@ class RepositoryTagImages(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/revert')
|
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/restore')
|
||||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
@path_param('tag', 'The name of the tag')
|
@path_param('tag', 'The name of the tag')
|
||||||
class RevertTag(RepositoryParamResource):
|
class RestoreTag(RepositoryParamResource):
|
||||||
""" Resource for reverting a repository tag back to a previous image. """
|
""" Resource for restoring a repository tag back to a previous image. """
|
||||||
schemas = {
|
schemas = {
|
||||||
'RevertTag': {
|
'RestoreTag': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'description': 'Reverts a tag to a specific image',
|
'description': 'Restores a tag to a specific image',
|
||||||
'required': [
|
'required': [
|
||||||
'image',
|
'image',
|
||||||
],
|
],
|
||||||
|
@ -197,32 +197,45 @@ class RevertTag(RepositoryParamResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'Image identifier to which the tag should point',
|
'description': 'Image identifier to which the tag should point',
|
||||||
},
|
},
|
||||||
|
'manifest_digest': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'If specified, the manifest digest that should be used',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@nickname('revertTag')
|
@nickname('restoreTag')
|
||||||
@validate_json_request('RevertTag')
|
@validate_json_request('RestoreTag')
|
||||||
def post(self, namespace, repository, tag):
|
def post(self, namespace, repository, tag):
|
||||||
""" Reverts a repository tag back to a previous image in the repository. """
|
""" Restores a repository tag back to a previous image in the repository. """
|
||||||
try:
|
repo = model.repository.get_repository(namespace, repository)
|
||||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
|
||||||
except model.DataModelException:
|
|
||||||
raise NotFound()
|
|
||||||
|
|
||||||
# Revert the tag back to the previous image.
|
# Restore the tag back to the previous image.
|
||||||
image_id = request.get_json()['image']
|
image_id = request.get_json()['image']
|
||||||
model.tag.revert_tag(tag_image.repository, tag, image_id)
|
manifest_digest = request.get_json().get('manifest_digest', None)
|
||||||
|
if manifest_digest is not None:
|
||||||
|
existing_image = model.tag.restore_tag_to_manifest(repo, tag, manifest_digest)
|
||||||
|
else:
|
||||||
|
existing_image = model.tag.restore_tag_to_image(repo, tag, image_id)
|
||||||
|
|
||||||
# Log the reversion.
|
# Log the reversion/restoration.
|
||||||
username = get_authenticated_user().username
|
username = get_authenticated_user().username
|
||||||
|
log_data = {
|
||||||
|
'username': username,
|
||||||
|
'repo': repository,
|
||||||
|
'tag': tag,
|
||||||
|
'image': image_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing_image is not None:
|
||||||
|
log_data['original_image'] = existing_image.docker_image_id
|
||||||
|
|
||||||
log_action('revert_tag', namespace,
|
log_action('revert_tag', namespace,
|
||||||
{'username': username, 'repo': repository, 'tag': tag,
|
log_data, repo=model.repository.get_repository(namespace, repository))
|
||||||
'image': image_id, 'original_image': tag_image.docker_image_id},
|
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'image_id': image_id,
|
'image_id': image_id,
|
||||||
'original_image_id': tag_image.docker_image_id
|
'original_image_id': existing_image.docker_image_id if existing_image else None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ from endpoints.api import api_bp, api
|
||||||
|
|
||||||
from endpoints.api.team import (TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite,
|
from endpoints.api.team import (TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite,
|
||||||
TeamPermissions)
|
TeamPermissions)
|
||||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RevertTag
|
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RestoreTag
|
||||||
from endpoints.api.search import EntitySearch
|
from endpoints.api.search import EntitySearch
|
||||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||||
|
@ -2539,10 +2539,10 @@ class TestRepositoryImage5avqBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('GET', 404, 'devtable', None)
|
self._run_test('GET', 404, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTagHp8rPublicPublicrepo(ApiTestCase):
|
class TestRestoreTagHp8rPublicPublicrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RevertTag, tag="HP8R", repository="public/publicrepo")
|
self._set_url(RestoreTag, tag="HP8R", repository="public/publicrepo")
|
||||||
|
|
||||||
def test_post_anonymous(self):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
||||||
|
@ -2557,10 +2557,10 @@ class TestRevertTagHp8rPublicPublicrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'devtable', {u'image': 'WXNG'})
|
self._run_test('POST', 403, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTagHp8rDevtableShared(ApiTestCase):
|
class TestRestoreTagHp8rDevtableShared(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RevertTag, tag="HP8R", repository="devtable/shared")
|
self._set_url(RestoreTag, tag="HP8R", repository="devtable/shared")
|
||||||
|
|
||||||
def test_post_anonymous(self):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
||||||
|
@ -2572,13 +2572,13 @@ class TestRevertTagHp8rDevtableShared(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', {u'image': 'WXNG'})
|
self._run_test('POST', 400, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTagHp8rBuynlargeOrgrepo(ApiTestCase):
|
class TestRestoreTagHp8rBuynlargeOrgrepo(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
self._set_url(RevertTag, tag="HP8R", repository="buynlarge/orgrepo")
|
self._set_url(RestoreTag, tag="HP8R", repository="buynlarge/orgrepo")
|
||||||
|
|
||||||
def test_post_anonymous(self):
|
def test_post_anonymous(self):
|
||||||
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
self._run_test('POST', 401, None, {u'image': 'WXNG'})
|
||||||
|
@ -2590,7 +2590,7 @@ class TestRevertTagHp8rBuynlargeOrgrepo(ApiTestCase):
|
||||||
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
self._run_test('POST', 403, 'reader', {u'image': 'WXNG'})
|
||||||
|
|
||||||
def test_post_devtable(self):
|
def test_post_devtable(self):
|
||||||
self._run_test('POST', 404, 'devtable', {u'image': 'WXNG'})
|
self._run_test('POST', 400, 'devtable', {u'image': 'WXNG'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ from util.secscan.fake import fake_security_scanner
|
||||||
|
|
||||||
from endpoints.api.team import (TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam,
|
from endpoints.api.team import (TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam,
|
||||||
TeamPermissions, InviteTeamMember)
|
TeamPermissions, InviteTeamMember)
|
||||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags
|
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RestoreTag, ListRepositoryTags
|
||||||
from endpoints.api.search import EntitySearch, ConductSearch
|
from endpoints.api.search import EntitySearch, ConductSearch
|
||||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||||
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildList, RepositoryBuildResource
|
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildList, RepositoryBuildResource
|
||||||
|
@ -2872,24 +2872,32 @@ class TestGetImageChanges(ApiTestCase):
|
||||||
# image_id=image_id))
|
# image_id=image_id))
|
||||||
|
|
||||||
|
|
||||||
class TestRevertTag(ApiTestCase):
|
class TestRestoreTag(ApiTestCase):
|
||||||
def test_reverttag_invalidtag(self):
|
def test_restoretag_invalidtag(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
self.postResponse(RevertTag,
|
self.postResponse(RestoreTag,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='invalidtag'),
|
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='invalidtag'),
|
||||||
data=dict(image='invalid_image'),
|
data=dict(image='invalid_image'),
|
||||||
expected_code=404)
|
expected_code=400)
|
||||||
|
|
||||||
def test_reverttag_invalidimage(self):
|
def test_restoretag_invalidimage(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
self.postResponse(RevertTag,
|
self.postResponse(RestoreTag,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='latest'),
|
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='latest'),
|
||||||
data=dict(image='invalid_image'),
|
data=dict(image='invalid_image'),
|
||||||
expected_code=400)
|
expected_code=400)
|
||||||
|
|
||||||
def test_reverttag(self):
|
def test_restoretag_invalidmanifest(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
self.postResponse(RestoreTag,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='latest'),
|
||||||
|
data=dict(manifest_digest='invalid_digest'),
|
||||||
|
expected_code=400)
|
||||||
|
|
||||||
|
def test_restoretag(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
json = self.getJsonResponse(ListRepositoryTags,
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
@ -2901,8 +2909,8 @@ class TestRevertTag(ApiTestCase):
|
||||||
|
|
||||||
previous_image_id = json['tags'][1]['docker_image_id']
|
previous_image_id = json['tags'][1]['docker_image_id']
|
||||||
|
|
||||||
self.postJsonResponse(RevertTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
self.postJsonResponse(RestoreTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
tag='latest'),
|
tag='latest'),
|
||||||
data=dict(image=previous_image_id))
|
data=dict(image=previous_image_id))
|
||||||
|
|
||||||
json = self.getJsonResponse(ListRepositoryTags,
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
@ -2912,6 +2920,29 @@ class TestRevertTag(ApiTestCase):
|
||||||
self.assertFalse('end_ts' in json['tags'][0])
|
self.assertFalse('end_ts' in json['tags'][0])
|
||||||
self.assertEquals(previous_image_id, json['tags'][0]['docker_image_id'])
|
self.assertEquals(previous_image_id, json['tags'][0]['docker_image_id'])
|
||||||
|
|
||||||
|
def test_restoretag_to_digest(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
|
tag='latest'))
|
||||||
|
|
||||||
|
self.assertEquals(2, len(json['tags']))
|
||||||
|
self.assertFalse('end_ts' in json['tags'][0])
|
||||||
|
|
||||||
|
previous_manifest = json['tags'][1]['manifest_digest']
|
||||||
|
|
||||||
|
self.postJsonResponse(RestoreTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
|
tag='latest'),
|
||||||
|
data=dict(image='foo', manifest_digest=previous_manifest))
|
||||||
|
|
||||||
|
json = self.getJsonResponse(ListRepositoryTags,
|
||||||
|
params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||||
|
tag='latest'))
|
||||||
|
self.assertEquals(3, len(json['tags']))
|
||||||
|
self.assertFalse('end_ts' in json['tags'][0])
|
||||||
|
self.assertEquals(previous_manifest, json['tags'][0]['manifest_digest'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestListAndDeleteTag(ApiTestCase):
|
class TestListAndDeleteTag(ApiTestCase):
|
||||||
|
|
Reference in a new issue