Change revert tag into restore tag and add manifest support

This commit is contained in:
Joseph Schorr 2017-03-03 17:23:23 -05:00
parent 35752176b5
commit e90cab4d77
4 changed files with 137 additions and 56 deletions

View file

@ -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)

View file

@ -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,
}

View file

@ -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'})

View file

@ -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):