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
|
||||
|
||||
|
||||
def revert_tag(repo_obj, tag_name, docker_image_id):
|
||||
""" Reverts a tag to a specific image ID. """
|
||||
# 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 revert to unknown or invalid image')
|
||||
def restore_tag_to_manifest(repo_obj, tag_name, manifest_digest):
|
||||
""" Restores a tag to a specific manifest digest. """
|
||||
with db_transaction():
|
||||
# Verify that the manifest digest already existed under this repository under the
|
||||
# tag.
|
||||
try:
|
||||
manifest = (TagManifest
|
||||
.select(TagManifest, RepositoryTag, Image)
|
||||
.join(RepositoryTag)
|
||||
.join(Image)
|
||||
.where(RepositoryTag.repository == repo_obj)
|
||||
.where(RepositoryTag.name == tag_name)
|
||||
.where(TagManifest.digest == manifest_digest)
|
||||
.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,
|
||||
docker_image_id, reversion=True)
|
||||
# Lookup the existing image, if any.
|
||||
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,
|
||||
manifest_data):
|
||||
manifest_data, reversion=False):
|
||||
""" 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.
|
||||
"""
|
||||
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:
|
||||
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('tag', 'The name of the tag')
|
||||
class RevertTag(RepositoryParamResource):
|
||||
""" Resource for reverting a repository tag back to a previous image. """
|
||||
class RestoreTag(RepositoryParamResource):
|
||||
""" Resource for restoring a repository tag back to a previous image. """
|
||||
schemas = {
|
||||
'RevertTag': {
|
||||
'RestoreTag': {
|
||||
'type': 'object',
|
||||
'description': 'Reverts a tag to a specific image',
|
||||
'description': 'Restores a tag to a specific image',
|
||||
'required': [
|
||||
'image',
|
||||
],
|
||||
|
@ -197,32 +197,45 @@ class RevertTag(RepositoryParamResource):
|
|||
'type': 'string',
|
||||
'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
|
||||
@nickname('revertTag')
|
||||
@validate_json_request('RevertTag')
|
||||
@nickname('restoreTag')
|
||||
@validate_json_request('RestoreTag')
|
||||
def post(self, namespace, repository, tag):
|
||||
""" Reverts a repository tag back to a previous image in the repository. """
|
||||
try:
|
||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
||||
except model.DataModelException:
|
||||
raise NotFound()
|
||||
""" Restores a repository tag back to a previous image in the repository. """
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
|
||||
# Revert the tag back to the previous image.
|
||||
# Restore the tag back to the previous 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
|
||||
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,
|
||||
{'username': username, 'repo': repository, 'tag': tag,
|
||||
'image': image_id, 'original_image': tag_image.docker_image_id},
|
||||
repo=model.repository.get_repository(namespace, repository))
|
||||
log_data, repo=model.repository.get_repository(namespace, repository))
|
||||
|
||||
return {
|
||||
'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,
|
||||
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.image import RepositoryImage, RepositoryImageList
|
||||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||
|
@ -2539,10 +2539,10 @@ class TestRepositoryImage5avqBuynlargeOrgrepo(ApiTestCase):
|
|||
self._run_test('GET', 404, 'devtable', None)
|
||||
|
||||
|
||||
class TestRevertTagHp8rPublicPublicrepo(ApiTestCase):
|
||||
class TestRestoreTagHp8rPublicPublicrepo(ApiTestCase):
|
||||
def 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):
|
||||
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'})
|
||||
|
||||
|
||||
class TestRevertTagHp8rDevtableShared(ApiTestCase):
|
||||
class TestRestoreTagHp8rDevtableShared(ApiTestCase):
|
||||
def 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):
|
||||
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'})
|
||||
|
||||
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):
|
||||
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):
|
||||
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'})
|
||||
|
||||
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,
|
||||
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.image import RepositoryImage, RepositoryImageList
|
||||
from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildList, RepositoryBuildResource
|
||||
|
@ -2872,24 +2872,32 @@ class TestGetImageChanges(ApiTestCase):
|
|||
# image_id=image_id))
|
||||
|
||||
|
||||
class TestRevertTag(ApiTestCase):
|
||||
def test_reverttag_invalidtag(self):
|
||||
class TestRestoreTag(ApiTestCase):
|
||||
def test_restoretag_invalidtag(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
self.postResponse(RevertTag,
|
||||
self.postResponse(RestoreTag,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='invalidtag'),
|
||||
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.postResponse(RevertTag,
|
||||
self.postResponse(RestoreTag,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/history', tag='latest'),
|
||||
data=dict(image='invalid_image'),
|
||||
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)
|
||||
|
||||
json = self.getJsonResponse(ListRepositoryTags,
|
||||
|
@ -2901,8 +2909,8 @@ class TestRevertTag(ApiTestCase):
|
|||
|
||||
previous_image_id = json['tags'][1]['docker_image_id']
|
||||
|
||||
self.postJsonResponse(RevertTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||
tag='latest'),
|
||||
self.postJsonResponse(RestoreTag, params=dict(repository=ADMIN_ACCESS_USER + '/history',
|
||||
tag='latest'),
|
||||
data=dict(image=previous_image_id))
|
||||
|
||||
json = self.getJsonResponse(ListRepositoryTags,
|
||||
|
@ -2912,6 +2920,29 @@ class TestRevertTag(ApiTestCase):
|
|||
self.assertFalse('end_ts' in json['tags'][0])
|
||||
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):
|
||||
|
|
Reference in a new issue