Merge pull request #3306 from quay/fix-manifest-ui
Fix manifest UI page to properly show the layers of manifests and show manifest lists
This commit is contained in:
commit
fc691cefb4
15 changed files with 186 additions and 91 deletions
|
@ -13,6 +13,9 @@ class RegistryModelProxy(object):
|
||||||
self._model = oci_model if os.getenv('OCI_DATA_MODEL') == 'true' else pre_oci_model
|
self._model = oci_model if os.getenv('OCI_DATA_MODEL') == 'true' else pre_oci_model
|
||||||
|
|
||||||
def setup_split(self, v22_whitelist):
|
def setup_split(self, v22_whitelist):
|
||||||
|
if os.getenv('OCI_DATA_MODEL') == 'true':
|
||||||
|
return
|
||||||
|
|
||||||
logger.info('===============================')
|
logger.info('===============================')
|
||||||
logger.info('Enabling split registry model with namespace whitelist `%s`', v22_whitelist)
|
logger.info('Enabling split registry model with namespace whitelist `%s`', v22_whitelist)
|
||||||
logger.info('===============================')
|
logger.info('===============================')
|
||||||
|
|
|
@ -207,7 +207,14 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
|
||||||
@property
|
@property
|
||||||
@requiresinput('legacy_image')
|
@requiresinput('legacy_image')
|
||||||
def legacy_image(self, legacy_image):
|
def legacy_image(self, legacy_image):
|
||||||
""" Returns the legacy Docker V1-style image for this manifest. Note that this
|
""" Returns the legacy Docker V1-style image for this manifest.
|
||||||
|
"""
|
||||||
|
return legacy_image
|
||||||
|
|
||||||
|
@property
|
||||||
|
@optionalinput('legacy_image')
|
||||||
|
def legacy_image_if_present(self, legacy_image):
|
||||||
|
""" Returns the legacy Docker V1-style image for this manifest. Note that this
|
||||||
will be None for manifests that point to other manifests instead of images.
|
will be None for manifests that point to other manifests instead of images.
|
||||||
"""
|
"""
|
||||||
return legacy_image
|
return legacy_image
|
||||||
|
|
|
@ -194,13 +194,21 @@ class RegistryDataInterface(object):
|
||||||
def get_manifest_local_blobs(self, manifest, include_placements=False):
|
def get_manifest_local_blobs(self, manifest, include_placements=False):
|
||||||
""" Returns the set of local blobs for the given manifest or None if none. """
|
""" Returns the set of local blobs for the given manifest or None if none. """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_manifest_layers(self, manifest, storage, include_placements=False):
|
||||||
|
""" Returns an *ordered list* of the layers found in the manifest, starting at the base
|
||||||
|
and working towards the leaf, including the associated Blob and its placements
|
||||||
|
(if specified). The layer information in `layer_info` will be of type
|
||||||
|
`image.docker.types.ManifestImageLayer`. Should not be called for a manifest list.
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, storage,
|
def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, storage,
|
||||||
include_placements=False):
|
include_placements=False):
|
||||||
""" Returns an *ordered list* of the layers found in the parsed manifest, starting at the base
|
""" Returns an *ordered list* of the layers found in the parsed manifest, starting at the base
|
||||||
and working towards the leaf, including the associated Blob and its placements
|
and working towards the leaf, including the associated Blob and its placements
|
||||||
(if specified). The layer information in `layer_info` will be of type
|
(if specified). The layer information in `layer_info` will be of type
|
||||||
`image.docker.types.ManifestImageLayer`.
|
`image.docker.types.ManifestImageLayer`. Should not be called for a manifest list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
@ -115,7 +115,7 @@ class OCIModel(SharedModel, RegistryDataInterface):
|
||||||
legacy_image_id = database.ManifestLegacyImage.get(manifest=manifest).image.docker_image_id
|
legacy_image_id = database.ManifestLegacyImage.get(manifest=manifest).image.docker_image_id
|
||||||
legacy_image = self.get_legacy_image(repository_ref, legacy_image_id, include_parents=True)
|
legacy_image = self.get_legacy_image(repository_ref, legacy_image_id, include_parents=True)
|
||||||
except database.ManifestLegacyImage.DoesNotExist:
|
except database.ManifestLegacyImage.DoesNotExist:
|
||||||
return None
|
pass
|
||||||
|
|
||||||
return Manifest.for_manifest(manifest, legacy_image)
|
return Manifest.for_manifest(manifest, legacy_image)
|
||||||
|
|
||||||
|
@ -414,11 +414,7 @@ class OCIModel(SharedModel, RegistryDataInterface):
|
||||||
legacy_image = oci.shared.get_legacy_image_for_manifest(manifest)
|
legacy_image = oci.shared.get_legacy_image_for_manifest(manifest)
|
||||||
return Manifest.for_manifest(manifest, LegacyImage.for_image(legacy_image))
|
return Manifest.for_manifest(manifest, LegacyImage.for_image(legacy_image))
|
||||||
|
|
||||||
def list_manifest_layers(self, manifest, include_placements=False):
|
def list_manifest_layers(self, manifest, storage, include_placements=False):
|
||||||
""" Returns an *ordered list* of the layers found in the manifest, starting at the base and
|
|
||||||
working towards the leaf, including the associated Blob and its placements (if specified).
|
|
||||||
Returns None if the manifest could not be parsed and validated.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
manifest_obj = database.Manifest.get(id=manifest._db_id)
|
manifest_obj = database.Manifest.get(id=manifest._db_id)
|
||||||
except database.Manifest.DoesNotExist:
|
except database.Manifest.DoesNotExist:
|
||||||
|
@ -431,8 +427,8 @@ class OCIModel(SharedModel, RegistryDataInterface):
|
||||||
logger.exception('Could not parse and validate manifest `%s`', manifest._db_id)
|
logger.exception('Could not parse and validate manifest `%s`', manifest._db_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._list_manifest_layers(manifest_obj.repository_id, parsed, include_placements,
|
return self._list_manifest_layers(manifest_obj.repository_id, parsed, storage,
|
||||||
by_manifest=True)
|
include_placements, by_manifest=True)
|
||||||
|
|
||||||
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -479,11 +479,7 @@ class PreOCIModel(SharedModel, RegistryDataInterface):
|
||||||
|
|
||||||
return Manifest.for_tag_manifest(tag_manifest)
|
return Manifest.for_tag_manifest(tag_manifest)
|
||||||
|
|
||||||
def list_manifest_layers(self, manifest, include_placements=False):
|
def list_manifest_layers(self, manifest, storage, include_placements=False):
|
||||||
""" Returns an *ordered list* of the layers found in the manifest, starting at the base and
|
|
||||||
working towards the leaf, including the associated Blob and its placements (if specified).
|
|
||||||
Returns None if the manifest could not be parsed and validated.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
tag_manifest = database.TagManifest.get(id=manifest._db_id)
|
tag_manifest = database.TagManifest.get(id=manifest._db_id)
|
||||||
except database.TagManifest.DoesNotExist:
|
except database.TagManifest.DoesNotExist:
|
||||||
|
@ -497,7 +493,7 @@ class PreOCIModel(SharedModel, RegistryDataInterface):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
repo_ref = RepositoryReference.for_id(tag_manifest.tag.repository_id)
|
repo_ref = RepositoryReference.for_id(tag_manifest.tag.repository_id)
|
||||||
return self.list_parsed_manifest_layers(repo_ref, parsed, include_placements)
|
return self.list_parsed_manifest_layers(repo_ref, parsed, storage, include_placements)
|
||||||
|
|
||||||
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -12,6 +12,7 @@ from data.registry_model.datatype import FromDictionaryException
|
||||||
from data.registry_model.datatypes import (RepositoryReference, Blob, TorrentInfo, BlobUpload,
|
from data.registry_model.datatypes import (RepositoryReference, Blob, TorrentInfo, BlobUpload,
|
||||||
LegacyImage, ManifestLayer, DerivedImage)
|
LegacyImage, ManifestLayer, DerivedImage)
|
||||||
from image.docker.schema1 import ManifestException, DockerSchema1ManifestBuilder
|
from image.docker.schema1 import ManifestException, DockerSchema1ManifestBuilder
|
||||||
|
from image.docker.schema2 import EMPTY_LAYER_BLOB_DIGEST
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -344,14 +345,23 @@ class SharedModel:
|
||||||
working towards the leaf, including the associated Blob and its placements (if specified).
|
working towards the leaf, including the associated Blob and its placements (if specified).
|
||||||
Returns None if the manifest could not be parsed and validated.
|
Returns None if the manifest could not be parsed and validated.
|
||||||
"""
|
"""
|
||||||
|
assert not parsed.is_manifest_list
|
||||||
|
|
||||||
|
retriever = RepositoryContentRetriever(repo_id, storage)
|
||||||
|
requires_empty_blob = parsed.get_requires_empty_layer_blob(retriever)
|
||||||
|
|
||||||
storage_map = {}
|
storage_map = {}
|
||||||
if parsed.local_blob_digests:
|
blob_digests = list(parsed.local_blob_digests)
|
||||||
|
if requires_empty_blob:
|
||||||
|
blob_digests.append(EMPTY_LAYER_BLOB_DIGEST)
|
||||||
|
|
||||||
|
if blob_digests:
|
||||||
blob_query = model.storage.lookup_repo_storages_by_content_checksum(repo_id,
|
blob_query = model.storage.lookup_repo_storages_by_content_checksum(repo_id,
|
||||||
parsed.local_blob_digests,
|
blob_digests,
|
||||||
by_manifest=by_manifest)
|
by_manifest=by_manifest)
|
||||||
storage_map = {blob.content_checksum: blob for blob in blob_query}
|
storage_map = {blob.content_checksum: blob for blob in blob_query}
|
||||||
|
|
||||||
retriever = RepositoryContentRetriever(repo_id, storage)
|
|
||||||
layers = parsed.get_layers(retriever)
|
layers = parsed.get_layers(retriever)
|
||||||
if layers is None:
|
if layers is None:
|
||||||
logger.error('Could not load layers for manifest `%s`', parsed.digest)
|
logger.error('Could not load layers for manifest `%s`', parsed.digest)
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
""" Manage the manifests of a repository. """
|
""" Manage the manifests of a repository. """
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from app import label_validator
|
from app import label_validator, storage
|
||||||
from data.model import InvalidLabelKeyException, InvalidMediaTypeException
|
from data.model import InvalidLabelKeyException, InvalidMediaTypeException
|
||||||
from data.registry_model import registry_model
|
from data.registry_model import registry_model
|
||||||
from digest import digest_tools
|
from digest import digest_tools
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||||
RepositoryParamResource, log_action, validate_json_request,
|
RepositoryParamResource, log_action, validate_json_request,
|
||||||
path_param, parse_args, query_param, abort, api,
|
path_param, parse_args, query_param, abort, api,
|
||||||
disallow_for_app_repositories)
|
disallow_for_app_repositories, format_date)
|
||||||
from endpoints.api.image import image_dict
|
from endpoints.api.image import image_dict
|
||||||
from endpoints.exception import NotFound
|
from endpoints.exception import NotFound
|
||||||
from util.validation import VALID_LABEL_KEY_REGEX
|
from util.validation import VALID_LABEL_KEY_REGEX
|
||||||
|
@ -18,6 +21,9 @@ BASE_MANIFEST_ROUTE = '/v1/repository/<apirepopath:repository>/manifest/<regex("
|
||||||
MANIFEST_DIGEST_ROUTE = BASE_MANIFEST_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
MANIFEST_DIGEST_ROUTE = BASE_MANIFEST_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
||||||
ALLOWED_LABEL_MEDIA_TYPES = ['text/plain', 'application/json']
|
ALLOWED_LABEL_MEDIA_TYPES = ['text/plain', 'application/json']
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _label_dict(label):
|
def _label_dict(label):
|
||||||
return {
|
return {
|
||||||
'id': label.uuid,
|
'id': label.uuid,
|
||||||
|
@ -27,15 +33,45 @@ def _label_dict(label):
|
||||||
'media_type': label.media_type_name,
|
'media_type': label.media_type_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _layer_dict(manifest_layer, index):
|
||||||
|
# NOTE: The `command` in the layer is either a JSON string of an array (schema 1) or
|
||||||
|
# a single string (schema 2). The block below normalizes it to have the same format.
|
||||||
|
try:
|
||||||
|
command = json.loads(manifest_layer.command)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
command = [manifest_layer.command]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'index': index,
|
||||||
|
'compressed_size': manifest_layer.compressed_size,
|
||||||
|
'is_remote': manifest_layer.is_remote,
|
||||||
|
'urls': manifest_layer.urls,
|
||||||
|
'command': command,
|
||||||
|
'blob_digest': str(manifest_layer.blob_digest),
|
||||||
|
'created_datetime': format_date(manifest_layer.created_datetime),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _manifest_dict(manifest):
|
def _manifest_dict(manifest):
|
||||||
image = None
|
image = None
|
||||||
if manifest.legacy_image is not None:
|
if manifest.legacy_image_if_present is not None:
|
||||||
image = image_dict(manifest.legacy_image, with_history=True)
|
image = image_dict(manifest.legacy_image, with_history=True)
|
||||||
|
|
||||||
|
layers = None
|
||||||
|
if not manifest.is_manifest_list:
|
||||||
|
layers = registry_model.list_manifest_layers(manifest, storage)
|
||||||
|
if layers is None:
|
||||||
|
logger.debug('Missing layers for manifest `%s`', manifest.digest)
|
||||||
|
abort(404)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'digest': manifest.digest,
|
'digest': manifest.digest,
|
||||||
|
'is_manifest_list': manifest.is_manifest_list,
|
||||||
'manifest_data': manifest.manifest_bytes,
|
'manifest_data': manifest.manifest_bytes,
|
||||||
'image': image,
|
'image': image,
|
||||||
|
'layers': ([_layer_dict(lyr.layer_info, idx) for idx, lyr in enumerate(layers)]
|
||||||
|
if layers else None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
.image-view-layer-element {
|
.manifest-view-layer-element {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element .image-comment {
|
.manifest-view-layer-element .image-comment {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element .nondocker-command {
|
.manifest-view-layer-element .nondocker-command {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element .nondocker-command:before {
|
.manifest-view-layer-element .nondocker-command:before {
|
||||||
content: "\f120";
|
content: "\f120";
|
||||||
font-family: "FontAwesome";
|
font-family: "FontAwesome";
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element .image-layer-line {
|
.manifest-view-layer-element .image-layer-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
@ -31,15 +31,15 @@
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element.first .image-layer-line {
|
.manifest-view-layer-element.first .image-layer-line {
|
||||||
top: 20px;
|
top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element.last .image-layer-line {
|
.manifest-view-layer-element.last .image-layer-line {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element .image-layer-dot {
|
.manifest-view-layer-element .image-layer-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 14px;
|
top: 14px;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
|
@ -52,17 +52,17 @@
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element.first .image-layer-dot {
|
.manifest-view-layer-element.first .image-layer-dot {
|
||||||
background: #428bca;
|
background: #428bca;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.image-view-layer-element .dockerfile-command-element .label {
|
.manifest-view-layer-element .dockerfile-command-element .label {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-view-layer-element .dockerfile-command-element .command-title {
|
.manifest-view-layer-element .dockerfile-command-element .command-title {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
<div class="image-view-layer-element" ng-class="getClass()">
|
|
||||||
<div class="image-command">
|
|
||||||
<image-command command="image.command"></image-command>
|
|
||||||
</div>
|
|
||||||
<div class="image-layer-dot"></div>
|
|
||||||
<div class="image-layer-line"></div>
|
|
||||||
</div>
|
|
7
static/directives/manifest-view-layer.html
Normal file
7
static/directives/manifest-view-layer.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="manifest-view-layer-element" ng-class="getClass()">
|
||||||
|
<div class="image-command">
|
||||||
|
<image-command command="layer.command"></image-command>
|
||||||
|
</div>
|
||||||
|
<div class="image-layer-dot"></div>
|
||||||
|
<div class="image-layer-line"></div>
|
||||||
|
</div>
|
1
static/directives/manifest-view-manifest-link.html
Normal file
1
static/directives/manifest-view-manifest-link.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<manifest-link repository="item.repository" manifest-digest="item.digest"></manifest-link>
|
|
@ -298,7 +298,7 @@
|
||||||
<!-- Expanded View -->
|
<!-- Expanded View -->
|
||||||
<tr ng-if="expandedView" class="expanded-viewport" ng-class="{'manifest-list': tag.is_manifest_list}">
|
<tr ng-if="expandedView" class="expanded-viewport" ng-class="{'manifest-list': tag.is_manifest_list}">
|
||||||
<td class="checkbox-col"></td>
|
<td class="checkbox-col"></td>
|
||||||
<td class="labels-col" colspan="{{6 + (repository.trust_enabled ? 1 : 0) + (Features.SECURITY_SCANNER ? 1 : 0) }}">
|
<td class="labels-col" colspan="{{6 + (repository.trust_enabled ? 1 : 0) + (Features.SECURITY_SCANNER ? 1 : -1) }}">
|
||||||
<!-- Image ID -->
|
<!-- Image ID -->
|
||||||
<div class="image-id-row">
|
<div class="image-id-row">
|
||||||
<manifest-link repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></manifest-link>
|
<manifest-link repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></manifest-link>
|
||||||
|
|
|
@ -1,27 +1,26 @@
|
||||||
/**
|
/**
|
||||||
* An element which displays a single layer representing an image in the image view.
|
* An element which displays a single layer in the manifest view.
|
||||||
*/
|
*/
|
||||||
angular.module('quay').directive('imageViewLayer', function () {
|
angular.module('quay').directive('manifestViewLayer', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
templateUrl: '/static/directives/image-view-layer.html',
|
templateUrl: '/static/directives/manifest-view-layer.html',
|
||||||
replace: false,
|
replace: false,
|
||||||
transclude: true,
|
transclude: true,
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'image': '=image',
|
'manifest': '=manifest',
|
||||||
'images': '=images'
|
'layer': '=layer'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element) {
|
||||||
$scope.getClass = function() {
|
$scope.getClass = function() {
|
||||||
var index = $.inArray($scope.image, $scope.images);
|
if ($scope.layer.index == 0) {
|
||||||
if (index < 0) {
|
return 'last';
|
||||||
return 'first';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == $scope.images.length - 1) {
|
if ($scope.layer.index == $scope.manifest.layers.length - 1) {
|
||||||
return 'last';
|
return 'first';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
$scope.manifestResource = ApiService.getRepoManifestAsResource(params).get(function(manifest) {
|
$scope.manifestResource = ApiService.getRepoManifestAsResource(params).get(function(manifest) {
|
||||||
$scope.manifest = manifest;
|
$scope.manifest = manifest;
|
||||||
$scope.reversedHistory = manifest.image.history.reverse();
|
$scope.reversedLayers = manifest.layers ? manifest.layers.reverse() : null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,5 +57,30 @@
|
||||||
if (!Features.SECURITY_SCANNER) { return; }
|
if (!Features.SECURITY_SCANNER) { return; }
|
||||||
$scope.manifestPackageCounter++;
|
$scope.manifestPackageCounter++;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.manifestsOf = function(manifest) {
|
||||||
|
if (!manifest || !manifest.is_manifest_list) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest._mapped_manifests) {
|
||||||
|
// Calculate once and cache to avoid angular digest cycles.
|
||||||
|
var parsed_manifest = JSON.parse(manifest.manifest_data);
|
||||||
|
|
||||||
|
manifest._mapped_manifests = parsed_manifest.manifests.map(function(manifest) {
|
||||||
|
return {
|
||||||
|
'repository': $scope.repository,
|
||||||
|
'raw': manifest,
|
||||||
|
'os': manifest.platform.os,
|
||||||
|
'architecture': manifest.platform.architecture,
|
||||||
|
'size': manifest.size,
|
||||||
|
'digest': manifest.digest,
|
||||||
|
'description': `${manifest.platform.os} on ${manifest.platform.architecture}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest._mapped_manifests;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -10,51 +10,65 @@
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="cor-title-content">
|
<span class="cor-title-content">
|
||||||
<i class="fa fa-file fa-lg" style="margin-right: 10px"></i>
|
<i class="fa fa-lg" ng-class="{'fa-file': !manifest.is_manifest_list, 'fa-file-text-o': manifest.is_manifest_list}" style="margin-right: 10px"></i>
|
||||||
{{ manifest.digest.substr(7, 12) }}
|
{{ manifest.digest.substr(7, 12) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<cor-tab-panel orientation="vertical" cor-nav-tabs>
|
<!-- Manifest list -->
|
||||||
<cor-tabs>
|
<div ng-if="manifest.is_manifest_list">
|
||||||
<cor-tab tab-title="Layers" tab-id="layers">
|
<div class="co-main-content-panel">
|
||||||
<i class="fa ci-layers"></i>
|
<cor-table table-data="manifestsOf(manifest)" table-item-title="manifests" filter-fields="['digest', 'os', 'architecture']">
|
||||||
</cor-tab>
|
<cor-table-col datafield="digest" sortfield="digest" title="Manifest"
|
||||||
<cor-tab tab-title="Security Scan" tab-id="vulnerabilities"
|
templateurl="/static/directives/manifest-view-manifest-link.html"></cor-table-col>
|
||||||
tab-init="loadManifestSecurity()"
|
<cor-table-col datafield="os" sortfield="os" title="Operating System"></cor-table-col>
|
||||||
quay-show="Features.SECURITY_SCANNER">
|
<cor-table-col datafield="architecture" sortfield="architecture" title="Architecture"></cor-table-col>
|
||||||
<i class="fa fa-bug"></i>
|
</cor-table>
|
||||||
</cor-tab>
|
</div>
|
||||||
<cor-tab tab-title="Packages" tab-id="packages"
|
</div>
|
||||||
tab-init="loadManifestPackages()"
|
|
||||||
quay-show="Features.SECURITY_SCANNER">
|
|
||||||
<i class="fa ci-package"></i>
|
|
||||||
</cor-tab>
|
|
||||||
</cor-tabs>
|
|
||||||
|
|
||||||
<cor-tab-content>
|
<!-- Manifest -->
|
||||||
<!-- Layers -->
|
<div ng-if="!manifest.is_manifest_list">
|
||||||
<cor-tab-pane id="layers">
|
<cor-tab-panel orientation="vertical" cor-nav-tabs>
|
||||||
<h3>Manifest Layers</h3>
|
<cor-tabs>
|
||||||
<div class="image-view-layer" repository="repository" image="manifest.image" images="manifest.image.history"></div>
|
<cor-tab tab-title="Layers" tab-id="layers">
|
||||||
<div class="image-view-layer" repository="repository" image="parent" images="manifest.image.history"
|
<i class="fa ci-layers"></i>
|
||||||
ng-repeat="parent in reversedHistory"></div>
|
</cor-tab>
|
||||||
</cor-tab-pane>
|
<cor-tab tab-title="Security Scan" tab-id="vulnerabilities"
|
||||||
|
tab-init="loadManifestSecurity()"
|
||||||
|
quay-show="Features.SECURITY_SCANNER">
|
||||||
|
<i class="fa fa-bug"></i>
|
||||||
|
</cor-tab>
|
||||||
|
<cor-tab tab-title="Packages" tab-id="packages"
|
||||||
|
tab-init="loadManifestPackages()"
|
||||||
|
quay-show="Features.SECURITY_SCANNER">
|
||||||
|
<i class="fa ci-package"></i>
|
||||||
|
</cor-tab>
|
||||||
|
</cor-tabs>
|
||||||
|
|
||||||
<!-- Vulnerabilities -->
|
<cor-tab-content>
|
||||||
<cor-tab-pane id="vulnerabilities" quay-show="Features.SECURITY_SCANNER">
|
<!-- Layers -->
|
||||||
<div quay-require="['SECURITY_SCANNER']">
|
<cor-tab-pane id="layers">
|
||||||
<div class="manifest-vulnerability-view" repository="repository" manifest="manifest" is-enabled="manifestSecurityCounter"></div>
|
<h3>Manifest Layers</h3>
|
||||||
</div>
|
<div class="manifest-view-layer" repository="repository" layer="layer"
|
||||||
</cor-tab-pane>
|
manifest="manifest" ng-repeat="layer in reversedLayers"></div>
|
||||||
|
</cor-tab-pane>
|
||||||
|
|
||||||
<!-- Features -->
|
<!-- Vulnerabilities -->
|
||||||
<cor-tab-pane id="packages" quay-show="Features.SECURITY_SCANNER">
|
<cor-tab-pane id="vulnerabilities" quay-show="Features.SECURITY_SCANNER">
|
||||||
<div quay-require="['SECURITY_SCANNER']">
|
<div quay-require="['SECURITY_SCANNER']">
|
||||||
<div class="manifest-feature-view" repository="repository" manifest="manifest" is-enabled="manifestPackageCounter"></div>
|
<div class="manifest-vulnerability-view" repository="repository" manifest="manifest" is-enabled="manifestSecurityCounter"></div>
|
||||||
</div>
|
</div>
|
||||||
</cor-tab-pane>
|
</cor-tab-pane>
|
||||||
</cor-tab-content>
|
|
||||||
</cor-tab-panel>
|
<!-- Features -->
|
||||||
|
<cor-tab-pane id="packages" quay-show="Features.SECURITY_SCANNER">
|
||||||
|
<div quay-require="['SECURITY_SCANNER']">
|
||||||
|
<div class="manifest-feature-view" repository="repository" manifest="manifest" is-enabled="manifestPackageCounter"></div>
|
||||||
|
</div>
|
||||||
|
</cor-tab-pane>
|
||||||
|
</cor-tab-content>
|
||||||
|
</cor-tab-panel>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in a new issue