Fix manifest UI page to properly show the layers of manifests and show manifest lists

This commit is contained in:
Joseph Schorr 2018-12-10 15:33:59 -05:00
parent 8cd3740c69
commit 4106f5ce51
13 changed files with 162 additions and 89 deletions

View file

@ -13,6 +13,9 @@ class RegistryModelProxy(object):
self._model = oci_model if os.getenv('OCI_DATA_MODEL') == 'true' else pre_oci_model
def setup_split(self, v22_whitelist):
if os.getenv('OCI_DATA_MODEL') == 'true':
return
logger.info('===============================')
logger.info('Enabling split registry model with namespace whitelist `%s`', v22_whitelist)
logger.info('===============================')

View file

@ -207,7 +207,14 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
@property
@requiresinput('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.
"""
return legacy_image

View file

@ -194,6 +194,14 @@ class RegistryDataInterface(object):
def get_manifest_local_blobs(self, manifest, include_placements=False):
""" 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`.
"""
@abstractmethod
def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, storage,
include_placements=False):

View file

@ -115,7 +115,7 @@ class OCIModel(SharedModel, RegistryDataInterface):
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)
except database.ManifestLegacyImage.DoesNotExist:
return None
pass
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)
return Manifest.for_manifest(manifest, LegacyImage.for_image(legacy_image))
def list_manifest_layers(self, manifest, 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.
"""
def list_manifest_layers(self, manifest, storage, include_placements=False):
try:
manifest_obj = database.Manifest.get(id=manifest._db_id)
except database.Manifest.DoesNotExist:
@ -431,8 +427,8 @@ class OCIModel(SharedModel, RegistryDataInterface):
logger.exception('Could not parse and validate manifest `%s`', manifest._db_id)
return None
return self._list_manifest_layers(manifest_obj.repository_id, parsed, include_placements,
by_manifest=True)
return self._list_manifest_layers(manifest_obj.repository_id, parsed, storage,
include_placements, by_manifest=True)
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
"""

View file

@ -479,11 +479,7 @@ class PreOCIModel(SharedModel, RegistryDataInterface):
return Manifest.for_tag_manifest(tag_manifest)
def list_manifest_layers(self, manifest, 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.
"""
def list_manifest_layers(self, manifest, storage, include_placements=False):
try:
tag_manifest = database.TagManifest.get(id=manifest._db_id)
except database.TagManifest.DoesNotExist:
@ -497,7 +493,7 @@ class PreOCIModel(SharedModel, RegistryDataInterface):
return None
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):
"""

View file

@ -1,14 +1,16 @@
""" Manage the manifests of a repository. """
import json
from flask import request
from app import label_validator
from app import label_validator, storage
from data.model import InvalidLabelKeyException, InvalidMediaTypeException
from data.registry_model import registry_model
from digest import digest_tools
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
RepositoryParamResource, log_action, validate_json_request,
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.exception import NotFound
from util.validation import VALID_LABEL_KEY_REGEX
@ -27,15 +29,37 @@ def _label_dict(label):
'media_type': label.media_type_name,
}
def _manifest_dict(manifest):
image = None
if manifest.legacy_image is not None:
image = image_dict(manifest.legacy_image, with_history=True)
def _layer_dict(manifest_layer, index):
try:
command = json.loads(manifest_layer.command)
except (TypeError, ValueError):
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):
image = None
if manifest.legacy_image_if_present is not None:
image = image_dict(manifest.legacy_image, with_history=True)
layers = registry_model.list_manifest_layers(manifest, storage)
return {
'digest': manifest.digest,
'is_manifest_list': manifest.is_manifest_list,
'manifest_data': manifest.manifest_bytes,
'image': image,
'layers': ([_layer_dict(lyr.layer_info, idx) for idx, lyr in enumerate(layers)]
if layers else None),
}

View file

@ -1,19 +1,19 @@
.image-view-layer-element {
.manifest-view-layer-element {
position: relative;
padding: 10px;
padding-left: 40px;
}
.image-view-layer-element .image-comment {
.manifest-view-layer-element .image-comment {
margin-bottom: 10px;
}
.image-view-layer-element .nondocker-command {
.manifest-view-layer-element .nondocker-command {
font-family: monospace;
padding: 2px;
}
.image-view-layer-element .nondocker-command:before {
.manifest-view-layer-element .nondocker-command:before {
content: "\f120";
font-family: "FontAwesome";
font-size: 16px;
@ -21,7 +21,7 @@
color: #999;
}
.image-view-layer-element .image-layer-line {
.manifest-view-layer-element .image-layer-line {
position: absolute;
top: 0px;
bottom: 0px;
@ -31,15 +31,15 @@
width: 0px;
}
.image-view-layer-element.first .image-layer-line {
.manifest-view-layer-element.first .image-layer-line {
top: 20px;
}
.image-view-layer-element.last .image-layer-line {
.manifest-view-layer-element.last .image-layer-line {
height: 16px;
}
.image-view-layer-element .image-layer-dot {
.manifest-view-layer-element .image-layer-dot {
position: absolute;
top: 14px;
left: 5px;
@ -52,17 +52,17 @@
z-index: 2;
}
.image-view-layer-element.first .image-layer-dot {
.manifest-view-layer-element.first .image-layer-dot {
background: #428bca;
}
@media (max-width: 767px) {
.image-view-layer-element .dockerfile-command-element .label {
.manifest-view-layer-element .dockerfile-command-element .label {
position: relative;
display: block;
}
.image-view-layer-element .dockerfile-command-element .command-title {
.manifest-view-layer-element .dockerfile-command-element .command-title {
padding-top: 10px;
padding-left: 0px;
}

View file

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

View 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>

View file

@ -0,0 +1 @@
<manifest-link repository="item.repository" manifest-digest="item.digest"></manifest-link>

View file

@ -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 = {
priority: 0,
templateUrl: '/static/directives/image-view-layer.html',
templateUrl: '/static/directives/manifest-view-layer.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'image': '=image',
'images': '=images'
'manifest': '=manifest',
'layer': '=layer'
},
controller: function($scope, $element) {
$scope.getClass = function() {
var index = $.inArray($scope.image, $scope.images);
if (index < 0) {
return 'first';
if ($scope.layer.index == 0) {
return 'last';
}
if (index == $scope.images.length - 1) {
return 'last';
if ($scope.layer.index == $scope.manifest.layers.length - 1) {
return 'first';
}
return '';

View file

@ -30,7 +30,7 @@
$scope.manifestResource = ApiService.getRepoManifestAsResource(params).get(function(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; }
$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;
};
}
})();

View file

@ -10,51 +10,65 @@
</a>
</span>
<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) }}
</span>
</div>
<cor-tab-panel orientation="vertical" cor-nav-tabs>
<cor-tabs>
<cor-tab tab-title="Layers" tab-id="layers">
<i class="fa ci-layers"></i>
</cor-tab>
<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>
<!-- Manifest list -->
<div ng-if="manifest.is_manifest_list">
<div class="co-main-content-panel">
<cor-table table-data="manifestsOf(manifest)" table-item-title="manifests" filter-fields="['digest', 'os', 'architecture']">
<cor-table-col datafield="digest" sortfield="digest" title="Manifest"
templateurl="/static/directives/manifest-view-manifest-link.html"></cor-table-col>
<cor-table-col datafield="os" sortfield="os" title="Operating System"></cor-table-col>
<cor-table-col datafield="architecture" sortfield="architecture" title="Architecture"></cor-table-col>
</cor-table>
</div>
</div>
<cor-tab-content>
<!-- Layers -->
<cor-tab-pane id="layers">
<h3>Manifest Layers</h3>
<div class="image-view-layer" repository="repository" image="manifest.image" images="manifest.image.history"></div>
<div class="image-view-layer" repository="repository" image="parent" images="manifest.image.history"
ng-repeat="parent in reversedHistory"></div>
</cor-tab-pane>
<!-- Manifest -->
<div ng-if="!manifest.is_manifest_list">
<cor-tab-panel orientation="vertical" cor-nav-tabs>
<cor-tabs>
<cor-tab tab-title="Layers" tab-id="layers">
<i class="fa ci-layers"></i>
</cor-tab>
<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-pane id="vulnerabilities" quay-show="Features.SECURITY_SCANNER">
<div quay-require="['SECURITY_SCANNER']">
<div class="manifest-vulnerability-view" repository="repository" manifest="manifest" is-enabled="manifestSecurityCounter"></div>
</div>
</cor-tab-pane>
<cor-tab-content>
<!-- Layers -->
<cor-tab-pane id="layers">
<h3>Manifest Layers</h3>
<div class="manifest-view-layer" repository="repository" layer="layer"
manifest="manifest" ng-repeat="layer in reversedLayers"></div>
</cor-tab-pane>
<!-- 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>
<!-- Vulnerabilities -->
<cor-tab-pane id="vulnerabilities" quay-show="Features.SECURITY_SCANNER">
<div quay-require="['SECURITY_SCANNER']">
<div class="manifest-vulnerability-view" repository="repository" manifest="manifest" is-enabled="manifestSecurityCounter"></div>
</div>
</cor-tab-pane>
<!-- 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>