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,8 +324,38 @@ 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. """
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')
# 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 # Verify that the image ID already existed under this repository under the
# tag. # tag.
try: try:
@ -337,19 +367,26 @@ def revert_tag(repo_obj, tag_name, docker_image_id):
.where(Image.docker_image_id == docker_image_id) .where(Image.docker_image_id == docker_image_id)
.get()) .get())
except RepositoryTag.DoesNotExist: except RepositoryTag.DoesNotExist:
raise DataModelException('Cannot revert to unknown or invalid image') raise DataModelException('Cannot restore to unknown or invalid image')
return create_or_update_tag(repo_obj.namespace_user.username, repo_obj.name, tag_name, # 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) 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)

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('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,
} }

View file

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

View file

@ -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,7 +2909,7 @@ 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))
@ -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):