Switch from an image view UI to a manifest view UI

We no longer allow viewing individual images, but instead only manifests. This will help with the transition to Clair V3 (which is manifest based) and, eventually, the the new data model (which will also be manifest based)
This commit is contained in:
Joseph Schorr 2018-03-28 16:03:18 -04:00
parent d41dcaae23
commit fc6eb71ab1
24 changed files with 312 additions and 260 deletions

View file

@ -1,4 +1,5 @@
""" Manage the manifests of a repository. """
import json
from app import label_validator
from flask import request
@ -18,6 +19,22 @@ MANIFEST_DIGEST_ROUTE = BASE_MANIFEST_ROUTE.format(digest_tools.DIGEST_PATTERN)
ALLOWED_LABEL_MEDIA_TYPES = ['text/plain', 'application/json']
@resource(MANIFEST_DIGEST_ROUTE)
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('manifestref', 'The digest of the manifest')
class RepositoryManifest(RepositoryParamResource):
""" Resource for retrieving a specific repository manifest. """
@require_repo_read
@nickname('getRepoManifest')
@disallow_for_app_repositories
def get(self, namespace_name, repository_name, manifestref):
manifest = model.get_repository_manifest(namespace_name, repository_name, manifestref)
if manifest is None:
raise NotFound()
return manifest.to_dict()
@resource(MANIFEST_DIGEST_ROUTE + '/labels')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('manifestref', 'The digest of the manifest')

View file

@ -28,7 +28,21 @@ class ManifestLabel(
'source_type': self.source_type_name,
'media_type': self.media_type_name,
}
class ManifestAndImage(
namedtuple('ManifestAndImage', [
'digest',
'manifest_data',
'image',
])):
def to_dict(self):
return {
'digest': self.digest,
'manifest_data': self.manifest_data,
'image': self.image.to_dict(),
}
@add_metaclass(ABCMeta)
class ManifestLabelInterface(object):
@ -95,3 +109,9 @@ class ManifestLabelInterface(object):
Returns:
ManifestLabel or None
"""
@abstractmethod
def get_repository_manifest(self, namespace_name, repository_name, digest):
"""
Returns the manifest and image for the manifest with the specified digest, if any.
"""

View file

@ -1,5 +1,8 @@
from manifest_models_interface import ManifestLabel, ManifestLabelInterface
import json
from manifest_models_interface import ManifestLabel, ManifestLabelInterface, ManifestAndImage
from data import model
from image_models_pre_oci import pre_oci_model as image_models
class ManifestLabelPreOCI(ManifestLabelInterface):
@ -36,6 +39,20 @@ class ManifestLabelPreOCI(ManifestLabelInterface):
return self._label(model.label.delete_manifest_label(label_uuid, tag_manifest))
def get_repository_manifest(self, namespace_name, repository_name, digest):
try:
tag_manifest = model.tag.load_manifest_by_digest(namespace_name, repository_name, digest)
except model.DataModelException:
return None
# TODO: remove this dependency on image once we've moved to the new data model.
image = image_models.get_repository_image(namespace_name, repository_name,
tag_manifest.tag.image.docker_image_id)
manifest_data = json.loads(tag_manifest.json_data)
return ManifestAndImage(digest=digest, manifest_data=manifest_data, image=image)
def _label(self, label_obj):
if not label_obj:
return None

View file

@ -0,0 +1,22 @@
import pytest
from data import model
from endpoints.api.manifest import RepositoryManifest
from endpoints.api.test.shared import conduct_api_call
from endpoints.test.shared import client_with_identity
from test.fixtures import *
def test_repository_manifest(client):
with client_with_identity('devtable', client) as cl:
tags = model.tag.list_repository_tags('devtable', 'simple')
digests = model.tag.get_tag_manifest_digests(tags)
for tag in tags:
manifest = digests[tag.id]
params = {
'repository': 'devtable/simple',
'manifestref': manifest,
}
result = conduct_api_call(cl, RepositoryManifest, 'GET', params, None, 200).json
assert result['digest'] == manifest
assert result['manifest_data']
assert result['image']

View file

@ -15,6 +15,7 @@ from endpoints.api.search import ConductRepositorySearch
from endpoints.api.superuser import SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource
from endpoints.api.superuser import SuperUserRepositoryBuildStatus
from endpoints.api.appspecifictokens import AppTokens, AppToken
from endpoints.api.manifest import RepositoryManifest
from endpoints.api.trigger import BuildTrigger
from endpoints.test.shared import client_with_identity, toggle_feature
@ -28,6 +29,7 @@ SEARCH_PARAMS = {'query': ''}
NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', 'uuid': 'some uuid'}
TOKEN_PARAMS = {'token_uuid': 'someuuid'}
TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_uuid': 'someuuid'}
MANIFEST_PARAMS = {'repository': 'devtable/simple', 'manifestref': 'sha256:deadbeef'}
@pytest.mark.parametrize('resource,method,params,body,identity,expected', [
(AppTokens, 'GET', {}, {}, None, 401),
@ -50,6 +52,11 @@ TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_uuid': 'someuuid'}
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'reader', 404),
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'devtable', 404),
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, None, 401),
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, 'freshuser', 403),
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, 'reader', 403),
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, 'devtable', 404),
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, None, 401),
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'freshuser', 403),
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'reader', 403),