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:
parent
d41dcaae23
commit
fc6eb71ab1
24 changed files with 312 additions and 260 deletions
|
@ -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')
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
22
endpoints/api/test/test_manifest.py
Normal file
22
endpoints/api/test/test_manifest.py
Normal 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']
|
|
@ -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),
|
||||
|
|
Reference in a new issue