Merge pull request #3043 from quay/joseph.schorr/QUAY-897/manifest-focus

Change from image view UI to manifest view UI
This commit is contained in:
josephschorr 2018-05-22 13:23:55 -04:00 committed by GitHub
commit 0c1b13828f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 428 additions and 379 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

@ -30,6 +30,20 @@ class ManifestLabel(
}
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),

View file

@ -1,29 +1,7 @@
.image-view-layer-element {
position: relative;
padding: 10px;
padding-left: 170px;
}
.image-view-layer-element .image-id {
font-family: monospace;
position: absolute;
top: 10px;
left: 10px;
width: 110px;
text-align: right;
}
.image-view-layer-element .image-id a {
color: #aaa;
}
.image-view-layer-element.first .image-id {
font-weight: bold;
font-size: 110%;
}
.image-view-layer-element.first .image-id a {
color: black;
padding-left: 40px;
}
.image-view-layer-element .image-comment {
@ -47,7 +25,7 @@
position: absolute;
top: 0px;
bottom: 0px;
left: 140px;
left: 10px;
border-left: 2px solid #428bca;
width: 0px;
@ -64,7 +42,7 @@
.image-view-layer-element .image-layer-dot {
position: absolute;
top: 14px;
left: 135px;
left: 5px;
border: 2px solid #428bca;
border-radius: 50%;
width: 12px;
@ -79,14 +57,6 @@
}
@media (max-width: 767px) {
.image-view-layer-element {
margin-left: -140px;
}
.image-view-layer-element .image-id {
display: none;
}
.image-view-layer-element .dockerfile-command-element .label {
position: relative;
display: block;

View file

@ -1,4 +1,4 @@
.image-feature-view-element .donut-icon {
.manifest-feature-view-element .donut-icon {
position: absolute;
top: 60px;
left: 95px;
@ -8,26 +8,26 @@
margin-left: -6px;
}
.image-feature-view-element > .empty {
.manifest-feature-view-element > .empty {
margin-top: 20px;
}
.image-feature-view-element .no-vulns {
.manifest-feature-view-element .no-vulns {
color: #2FC98E;
}
.image-feature-view-element .no-vulns i.fa {
.manifest-feature-view-element .no-vulns i.fa {
margin-right: 6px;
}
.image-feature-view-element .security-header {
.manifest-feature-view-element .security-header {
margin-top: -4px;
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.image-feature-view-element .donut-col {
.manifest-feature-view-element .donut-col {
padding-top: 20px;
text-align: center;
max-width: 250px;
@ -36,75 +36,75 @@
}
.image-feature-view-element #featureDonutChart {
.manifest-feature-view-element #featureDonutChart {
display: inline-block;
}
.image-feature-view-element .summary-col {
.manifest-feature-view-element .summary-col {
font-size: 18px;
display: inline-block;
vertical-align: top;
padding-top: 30px;
}
.image-feature-view-element .summary-col .title-item {
.manifest-feature-view-element .summary-col .title-item {
font-size: 24px;
margin-bottom: 30px;
}
.image-feature-view-element .summary-list {
.manifest-feature-view-element .summary-list {
text-align: left;
list-style: none;
}
.image-feature-view-element .summary-list i.fa {
.manifest-feature-view-element .summary-list i.fa {
margin-right: 10px;
}
.image-feature-view-element .summary-list .package-item strong {
.manifest-feature-view-element .summary-list .package-item strong {
text-align: right;
width: 40px;
display: inline-block;
margin-right: 6px;
}
.image-feature-view-element .co-table .empty {
.manifest-feature-view-element .co-table .empty {
color: #ddd;
}
.image-feature-view-element .co-table .single-col {
.manifest-feature-view-element .co-table .single-col {
width: 12.5%;
}
.image-feature-view-element .co-table .double-col {
.manifest-feature-view-element .co-table .double-col {
width: 25%;
}
.image-feature-view-element .co-table .impact-col {
.manifest-feature-view-element .co-table .impact-col {
text-align: center;
width: 130px;
}
.image-feature-view-element .co-table .image-col {
.manifest-feature-view-element .co-table .image-col {
white-space: nowrap;
}
.image-feature-view-element .co-table .image-col .fa {
.manifest-feature-view-element .co-table .image-col .fa {
margin-left: 6px;
opacity: 0.5;
}
@media (max-width: 767px) {
.image-feature-view-element .co-table .single-col {
.manifest-feature-view-element .co-table .single-col {
width: auto !important;
}
}
.image-feature-view-element .dockerfile-command {
.manifest-feature-view-element .dockerfile-command {
cursor: default;
}
.image-feature-view-element .dockerfile-command .command-title {
.manifest-feature-view-element .dockerfile-command .command-title {
font-size: 12px;
max-width: 297px;
overflow: hidden;
@ -114,11 +114,11 @@
vertical-align: middle;
}
.image-feature-view-element .vuln-summary i.fa {
.manifest-feature-view-element .vuln-summary i.fa {
margin-right: 6px;
}
.image-feature-view-element .defcon1 {
.manifest-feature-view-element .defcon1 {
background-color: #FB5151;
color: white;
}

View file

@ -1,16 +1,16 @@
.image-link {
.manifest-link {
display: inline-block;
white-space: nowrap;
width: 120px;
}
.image-link a {
.manifest-link a {
font-family: Consolas, "Lucida Console", Monaco, monospace;
font-size: 12px;
font-size: 10px;
text-decoration: none;
}
.image-link .id-label {
.manifest-link .id-label {
font-size: 10px;
cursor: pointer;
padding: 2px;
@ -24,6 +24,6 @@
display: inline-block;
}
.image-link .id-label.cas {
.manifest-link .id-label.cas {
background-color: #e8f1f6;
}

View file

@ -1,4 +1,4 @@
.image-vulnerability-view-element .donut-icon {
.manifest-vulnerability-view-element .donut-icon {
position: absolute;
top: 70px;
left: 95px;
@ -8,18 +8,18 @@
margin-left: -6px;
}
.image-vulnerability-view-element > .empty {
.manifest-vulnerability-view-element > .empty {
margin-top: 20px;
}
.image-vulnerability-view-element .security-header {
.manifest-vulnerability-view-element .security-header {
margin-top: -4px;
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.image-vulnerability-view-element .donut-col {
.manifest-vulnerability-view-element .donut-col {
padding-top: 20px;
text-align: center;
max-width: 250px;
@ -27,48 +27,48 @@
vertical-align: top;
}
.image-vulnerability-view-element #vulnDonutChart {
.manifest-vulnerability-view-element #vulnDonutChart {
display: inline-block;
}
.image-vulnerability-view-element .summary-col {
.manifest-vulnerability-view-element .summary-col {
font-size: 18px;
display: inline-block;
vertical-align: top;
padding-top: 30px;
}
.image-vulnerability-view-element .summary-col .title-item {
.manifest-vulnerability-view-element .summary-col .title-item {
font-size: 24px;
margin-bottom: 6px;
}
.image-vulnerability-view-element .summary-col .subtitle-item {
.manifest-vulnerability-view-element .summary-col .subtitle-item {
font-size: 22px;
margin-bottom: 6px;
}
.image-vulnerability-view-element .summary-list {
.manifest-vulnerability-view-element .summary-list {
text-align: left;
list-style: none;
}
.image-vulnerability-view-element .summary-list i.fa {
.manifest-vulnerability-view-element .summary-list i.fa {
margin-right: 10px;
}
.image-vulnerability-view-element .summary-list li.severity-item strong {
.manifest-vulnerability-view-element .summary-list li.severity-item strong {
text-align: right;
width: 40px;
display: inline-block;
margin-right: 6px;
}
.image-vulnerability-view-element .dockerfile-command {
.manifest-vulnerability-view-element .dockerfile-command {
cursor: default;
}
.image-vulnerability-view-element .dockerfile-command .command-title {
.manifest-vulnerability-view-element .dockerfile-command .command-title {
font-size: 12px;
max-width: 297px;
overflow: hidden;
@ -78,53 +78,53 @@
vertical-align: middle;
}
.image-vulnerability-view-element .co-table .empty {
.manifest-vulnerability-view-element .co-table .empty {
color: #ddd;
}
.image-vulnerability-view-element .co-table .single-col {
.manifest-vulnerability-view-element .co-table .single-col {
width: 15%;
}
.image-vulnerability-view-element .co-table .double-col {
.manifest-vulnerability-view-element .co-table .double-col {
width: 30%;
}
.image-vulnerability-view-element .co-table .impact-col {
.manifest-vulnerability-view-element .co-table .impact-col {
text-align: center;
width: 130px;
}
.image-vulnerability-view-element .co-table .nowrap-col {
.manifest-vulnerability-view-element .co-table .nowrap-col {
white-space: nowrap;
}
.image-vulnerability-view-element .co-table .image-col {
.manifest-vulnerability-view-element .co-table .image-col {
white-space: nowrap;
}
.image-vulnerability-view-element .co-table .image-col .fa {
.manifest-vulnerability-view-element .co-table .image-col .fa {
margin-left: 6px;
opacity: 0.5;
}
@media (max-width: 767px) {
.image-vulnerability-view-element .co-table .single-col {
.manifest-vulnerability-view-element .co-table .single-col {
width: auto !important;
}
}
.image-vulnerability-view-element .fixed-in-version:before {
.manifest-vulnerability-view-element .fixed-in-version:before {
font-family: FontAwesome;
content: '\f0a9';
margin-right: 6px;
}
.image-vulnerability-view-element .fixed-in-version {
.manifest-vulnerability-view-element .fixed-in-version {
color: rgb(47, 201, 142);
}
.image-vulnerability-view-element .cvss-text {
.manifest-vulnerability-view-element .cvss-text {
display: inline-block;
width: 40px;
text-align: right;
@ -135,11 +135,11 @@
color: #ccc;
}
.image-vulnerability-view-element .vulnerability-priority-view {
.manifest-vulnerability-view-element .vulnerability-priority-view {
margin-left: 10px;
}
.image-vulnerability-view-element .cvss {
.manifest-vulnerability-view-element .cvss {
display: inline-block;
width: 100px;
height: 10px;
@ -148,7 +148,7 @@
position: relative;;
}
.image-vulnerability-view-element .cvss span {
.manifest-vulnerability-view-element .cvss span {
display: inline-block;
position: absolute;
top: 0px;
@ -156,12 +156,12 @@
bottom: 0px;
}
.image-vulnerability-view-element .expansion-col {
.manifest-vulnerability-view-element .expansion-col {
padding-top: 20px;
padding-bottom: 20px;
}
.image-vulnerability-view-element .subtitle {
.manifest-vulnerability-view-element .subtitle {
color: #999;
font-size: 90%;
text-transform: uppercase;
@ -172,37 +172,37 @@
}
.image-vulnerability-view-element .expand-link {
.manifest-vulnerability-view-element .expand-link {
color: black !important;
}
.image-vulnerability-view-element .external-link {
.manifest-vulnerability-view-element .external-link {
margin-left: 10px;
font-size: 12px;
}
.image-vulnerability-view-element .description {
.manifest-vulnerability-view-element .description {
display: inline-block;
max-width: 1000px;
}
.image-vulnerability-view-element .asterisk {
.manifest-vulnerability-view-element .asterisk {
vertical-align: super;
font-size: 9px;
margin-left: 2px;
}
.image-vulnerability-view-element .severity-note {
.manifest-vulnerability-view-element .severity-note {
margin-bottom: 10px;
}
.image-vulnerability-view-element .severity-note .vulnerability-priority-view {
.manifest-vulnerability-view-element .severity-note .vulnerability-priority-view {
margin: 0px;
margin-left: 2px;
margin-right: 2px;
}
.image-vulnerability-view-element .defcon1 {
.manifest-vulnerability-view-element .defcon1 {
background-color: #FB5151;
color: white;
}

View file

@ -252,7 +252,7 @@
background: #F6FCFF;
}
.repo-tag-history-element .history-entry .image-link {
.repo-tag-history-element .history-entry .manifest-link {
margin-left: 6px;
}

View file

@ -1,70 +1,70 @@
.image-view .image-view-header {
.manifest-view .manifest-view-header {
padding: 10px;
background: #e8f1f6;
margin: -10px;
margin-bottom: 20px;
}
.image-view .image-view-header .section-icon {
.manifest-view .manifest-view-header .section-icon {
margin-right: 6px;
}
.image-view .image-view-header .section {
.manifest-view .manifest-view-header .section {
padding: 4px;
display: inline-block;
margin-right: 20px;
}
.image-view .co-tab-content {
.manifest-view .co-tab-content {
padding: 20px;
padding-top: 10px;
}
.image-view .co-tab-content h3 {
.manifest-view .co-tab-content h3 {
margin-bottom: 30px;
}
.image-view .fa-bug {
.manifest-view .fa-bug {
margin-right: 4px;
}
.image-view .co-filter-box {
.manifest-view .co-filter-box {
float: right;
min-width: 300px;
margin-bottom: 10px;
}
.image-view .co-filter-box .current-filtered {
.manifest-view .co-filter-box .current-filtered {
display: inline-block;
margin-right: 10px;
color: #999;
}
.image-view .co-filter-box input {
.manifest-view .co-filter-box input {
display: inline-block;
}
.image-view .level-col h4 {
.manifest-view .level-col h4 {
margin-top: 0px;
margin-bottom: 20px;
}
.image-view .levels {
.manifest-view .levels {
list-style: none;
padding: 0px;
margin: 0px;
}
.image-view .levels li {
.manifest-view .levels li {
margin-bottom: 20px;
}
.image-view .levels li .description {
.manifest-view .levels li .description {
margin-top: 6px;
font-size: 14px;
color: #999;
}
.image-view .level-col {
.manifest-view .level-col {
padding: 20px;
}

View file

@ -1,37 +0,0 @@
<span>
<span class="id-label" ng-if="!hasSHA256(manifestDigest)"
data-title="The Docker V1 ID for this image. This ID is not content addressable nor is it stable across pulls."
data-container="body"
ng-click="showCopyBox()"
bs-tooltip>V1ID</span>
<span class="id-label cas" ng-if="hasSHA256(manifestDigest)"
data-title="The content-addressable SHA256 hash of this tag."
data-container="body"
ng-click="showCopyBox()"
bs-tooltip>SHA256</span>
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ imageId }}"
class="image-link-element" bindonce>
<span ng-if="!hasSHA256(manifestDigest)">{{ imageId.substr(0, 12) }}</span>
<span ng-if="hasSHA256(manifestDigest)">{{ getShortDigest(manifestDigest) }}</span>
</a>
<div class="modal fade co-dialog" ng-if="showingCopyBox">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="hideCopyBox()"
aria-hidden="true">&times;</button>
<h4 class="modal-title"><span ng-if="hasSHA256(manifestDigest)">Manifest SHA256</span><span ng-if="!hasSHA256(manifestDigest)">V1 ID</span></h4>
</div>
<div class="modal-body">
<div class="copy-box" hovering-message="true" value="hasSHA256(manifestDigest) ? manifestDigest : imageId"></div>
</div>
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-default" ng-click="hideCopyBox()">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</span>

View file

@ -1,10 +1,5 @@
<div class="image-view-layer-element" ng-class="getClass()">
<div class="image-id">
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ image.id }}">
{{ image.id.substr(0, 12) }}
</a>
</div>
<div class="image-command">
<div class="image-command">
<image-command command="image.command"></image-command>
</div>
<div class="image-layer-dot"></div>

View file

@ -1,4 +1,4 @@
<div class="image-feature-view-element">
<div class="manifest-feature-view-element">
<!-- Unable to load -->
<div class="empty" ng-if="securityStatus == 'error'">
<div class="empty-icon">
@ -15,7 +15,7 @@
<div class="empty-icon">
<i class="fa fa-ellipsis-h"></i>
</div>
<div class="empty-primary-msg">This image has not been indexed yet</div>
<div class="empty-primary-msg">This manifest has not been indexed yet</div>
<div class="empty-secondary-msg">
Please try again in a few minutes.
</div>
@ -26,9 +26,9 @@
<div class="empty-icon">
<i class="fa fa-times-circle"></i>
</div>
<div class="empty-primary-msg">This image could not be indexed</div>
<div class="empty-primary-msg">This manifest could not be indexed</div>
<div class="empty-secondary-msg">
Quay security scanner was unable to index this image.
Quay security scanner was unable to index this manifest.
</div>
</div>
@ -38,9 +38,9 @@
<div class="empty-icon">
<i class="fa ci-package"></i>
</div>
<div class="empty-primary-msg">Image is not supported by Quay Security Scanner</div>
<div class="empty-primary-msg">Manifest is not supported by Quay Security Scanner</div>
<div class="empty-secondary-msg">
This image has an operating system or package manager unsupported by Quay Security Scanner.
This manifest has an operating system or package manager unsupported by Quay Security Scanner.
</div>
</div>
</div>
@ -79,7 +79,7 @@
</span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Packages...">
</span>
<h3>Image Packages</h3>
<h3>Packages</h3>
<!-- Table -->
<table class="co-table">
@ -159,17 +159,14 @@
</span>
<span bo-if="feature.vulnCount > 0 && feature.fixableScore > 0">
<span class="strength-indicator" value="feature.fixableScore" maximum="featuresInfo.highestFixableScore"
log-base="2"></span>
log-base="2"></span>
</span>
</td>
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
<span bo-if="feature.imageCommand">
<image-command command="feature.imageCommand"></image-command>
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ feature.imageId }}"><i class="fa fa-archive"></i></a>
</span>
<span bo-if="!feature.imageCommand">
<span class="image-link" repository="repository" image-id="feature.imageId"></span>
</span>
<span bo-if="!feature.imageCommand">(No Command)</span>
</td>
<td></td>
</tr>

View file

@ -1,4 +1,4 @@
<div class="image-vulnerability-view-element">
<div class="manifest-vulnerability-view-element">
<!-- Unable to load -->
<div class="empty" ng-if="securityStatus == 'error'">
<div class="empty-icon">
@ -15,7 +15,7 @@
<div class="empty-icon">
<i class="fa fa-ellipsis-h"></i>
</div>
<div class="empty-primary-msg">This image has not been indexed yet</div>
<div class="empty-primary-msg">This manifest has not been indexed yet</div>
<div class="empty-secondary-msg">
Please try again in a few minutes.
</div>
@ -26,9 +26,9 @@
<div class="empty-icon">
<i class="fa fa-times-circle"></i>
</div>
<div class="empty-primary-msg">This image could not be indexed</div>
<div class="empty-primary-msg">This manifest could not be indexed</div>
<div class="empty-secondary-msg">
Quay security scanner was unable to index this image.
Quay security scanner was unable to index this manifest.
</div>
</div>
@ -38,9 +38,9 @@
<div class="empty-icon">
<i class="fa fa-bug"></i>
</div>
<div class="empty-primary-msg">Image is not supported by Quay Security Scanner</div>
<div class="empty-primary-msg">Manifest is not supported by Quay Security Scanner</div>
<div class="empty-secondary-msg">
This image has an operating system or package manager unsupported by Quay Security Scanner.
This manifest has an operating system or package manager unsupported by Quay Security Scanner.
</div>
</div>
</div>
@ -72,7 +72,7 @@
</ul>
<div ng-if="!vulnerabilitiesInfo.severityBreakdown.length">
Quay Security Scanner has detected no vulnerabilities in this image.
Quay Security Scanner has detected no vulnerabilities in this manifest.
</div>
</div>
</div>
@ -87,13 +87,13 @@
<label><input type="checkbox" ng-model="options.fixableVulns">Only show fixable</label>
</div>
</span>
<h3>Image Vulnerabilities</h3>
<h3>Vulnerabilities</h3>
<!-- Table -->
<div class="empty" ng-if="!vulnerabilitiesInfo.vulnerabilities.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No vulnerabilities found.</div>
<div class="empty-secondary-msg">Quay Security Scanner has detected no vulnerabilities in this image.</div>
<div class="empty-secondary-msg">Quay Security Scanner has detected no vulnerabilities in this manifest.</div>
</div>
<table class="co-table" ng-show="vulnerabilitiesInfo.vulnerabilities.length">
@ -111,7 +111,7 @@
<td class="hidden-xs">Current version</td>
<td class="hidden-xs hidden-sm">Fixed in version</td>
</td>
<td class="hidden-xs hidden-sm hidden-md">Introduced in image</td>
<td class="hidden-xs hidden-sm hidden-md">Introduced in layer</td>
<td class="hidden-xs options-col"></td>
</thead>
<tbody ng-repeat="vuln in orderedVulnerabilities.visibleEntries" bindonce>
@ -152,12 +152,9 @@
</td>
<td class="double-col image-col hidden-xs hidden-sm hidden-md">
<span bo-if="vuln.imageCommand">
<image-command command="vuln.imageCommand"></image-command>
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ vuln.imageId }}"><i class="fa fa-archive"></i></a>
</span>
<span bo-if="!vuln.imageCommand">
<span class="image-link" repository="repository" image-id="vuln.imageId"></span>
<image-command command="feature.imageCommand"></image-command>
</span>
<span bo-if="!vuln.imageCommand">(No Command)</span>
</td>
<td></td>
</tr>
@ -181,8 +178,8 @@
</td>
</tr>
<tr>
<td>Introduced in Image:</td>
<td><span class="image-link" repository="repository" image-id="vuln.imageId"></span></td>
<td>Introduced in Layer:</td>
<td>{{ ::vuln.imageId }}</td>
</tr>
</table>
</div>

View file

@ -38,38 +38,38 @@
<span ng-switch on="entry.action">
<span ng-switch-when="recreate">
was recreated pointing to
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
manifest-digest="entry.manifest_digest"></manifest-link>
</span>
<span ng-switch-when="create">
was created pointing to
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
manifest-digest="entry.manifest_digest"></manifest-link>
</span>
<span ng-switch-when="delete">
was deleted
</span>
<span ng-switch-when="move">
was moved to
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
manifest-digest="entry.manifest_digest"></manifest-link>
from
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
manifest-digest="entry.old_manifest_digest"></manifest-link>
</span>
<span ng-switch-when="revert">
was reverted to
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
manifest-digest="entry.manifest_digest"></manifest-link>
from
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
manifest-digest="entry.old_manifest_digest"></manifest-link>
</span>
</span>
</div>
@ -87,21 +87,21 @@
<span ng-switch on="entry.action">
<a ng-switch-when="delete" ng-click="askRestoreTag(entry, true)">
Restore <span class="tag-span"><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.docker_image_id"
manifest-digest="entry.manifest_digest"></span>
manifest-digest="entry.manifest_digest"></manifest-link>
</a>
<a ng-switch-when="move" ng-click="askRestoreTag(entry, false)">
Revert <span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
manifest-digest="entry.old_manifest_digest"></manifest-link>
</a>
<a ng-switch-when="revert" ng-click="askRestoreTag(entry, false)">
Restore <span class="tag-span" data-title="{{ entry.tag_name }}" bs-tooltip><span>{{ entry.tag_name }}</span></span> to
<span class="image-link" repository="repository"
<manifest-link repository="repository"
image-id="entry.old_docker_image_id"
manifest-digest="entry.old_manifest_digest"></span>
manifest-digest="entry.old_manifest_digest"></manifest-link>
</a>
</span>
</div>

View file

@ -161,6 +161,14 @@
</span>
<span ng-if="!getTagVulnerabilities(tag).loading">
<!-- No Digest -->
<span class="nodigest" ng-if="getTagVulnerabilities(tag).status == 'nodigest'"
data-title="The tag does not have a V2 digest and so is unsupported for scan"
bs-tooltip>
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#eee'}]"></span>
Unsupported
</span>
<!-- Queued -->
<span class="scanning" ng-if="getTagVulnerabilities(tag).status == 'queued'"
data-title="The image for this tag is queued to be scanned for vulnerabilities"
@ -193,7 +201,7 @@
data-title="The image for this tag has no vulnerabilities as found in our database"
bs-tooltip
bindonce>
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities">
<a bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities">
<span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#2FC98E'}]"></span>
Passed
</a>
@ -204,7 +212,7 @@
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
class="has-vulns" bindonce>
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities"
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities"
data-title="This tag has {{ getTagVulnerabilities(tag).vulnerabilities.length }} vulnerabilities across {{ getTagVulnerabilities(tag).featuresInfo.brokenFeaturesCount }} packages"
bs-tooltip>
<!-- Donut -->
@ -218,7 +226,7 @@
</span>
</a>
<span class="dot" ng-if="getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length">&middot;</span>
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}?tab=vulnerabilities&fixable=true" ng-if="getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length">
<a class="vuln-link" bo-href-i="/repository/{{ repository.namespace }}/{{ repository.name }}/manifest/{{ tag.manifest_digest }}?tab=vulnerabilities&fixable=true" ng-if="getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length">
{{ getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length }} fixable
</a>
</span>
@ -237,9 +245,9 @@
<expiration-status-view expiration-date="tag.expiration_date" ng-if="repository.tag_operations_disabled || !repository.can_write"></expiration-status-view>
</td>
<!-- Image link -->
<!-- Manifest link -->
<td class="hidden-xs hidden-sm image-id-col">
<span class="image-link" repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></span>
<manifest-link repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></manifest-link>
</td>
<td class="hidden-xs hidden-sm hidden-md image-track"
ng-if="imageTracks.length > maxTrackCount" bindonce>
@ -293,7 +301,7 @@
<td class="labels-col" colspan="{{6 + (repository.trust_enabled ? 1 : 0) + (Features.SECURITY_SCANNER ? 1 : 0) }}">
<!-- Image ID -->
<div class="image-id-row">
<span class="image-link" repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></span>
<manifest-link repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></manifest-link>
</div>
<!-- Labels -->

View file

@ -162,9 +162,9 @@
Are you sure you want to restore tag
<span class="label label-default tag">{{ restoreTagInfo.tag.name }}</span> to image
<span class="image-link" repository="repository"
image-id="restoreTagInfo.image_id"
manifest-digest="restoreTagInfo.manifest_digest"></span>?
<manifest-link repository="repository"
image-id="restoreTagInfo.image_id"
manifest-digest="restoreTagInfo.manifest_digest"></manifest-link>?
</div>
<!-- Tag Operations Disabled Dialog -->

View file

@ -262,6 +262,10 @@ angular.module('quay').directive('repoPanelTags', function () {
};
$scope.getTagVulnerabilities = function(tag) {
if (!tag.manifest_digest) {
return 'nodigest';
}
return $scope.getImageVulnerabilities(tag.image_id);
};

View file

@ -1,47 +0,0 @@
/**
* An element which displays a link to a repository image.
*/
angular.module('quay').directive('imageLink', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/image-link.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'imageId': '=imageId',
'manifestDigest': '=?manifestDigest'
},
controller: function($scope, $element, $timeout) {
$scope.showingCopyBox = false;
$scope.hasSHA256 = function(digest) {
return digest && digest.indexOf('sha256:') == 0;
};
$scope.getShortDigest = function(digest) {
return digest.substr('sha256:'.length).substr(0, 12);
};
$scope.showCopyBox = function() {
$scope.showingCopyBox = true;
// Necessary to wait for digest cycle to complete.
$timeout(function() {
$element.find('.modal').modal('show');
}, 10);
};
$scope.hideCopyBox = function() {
$element.find('.modal').modal('hide');
// Wait for the modal to hide before removing from the DOM.
$timeout(function() {
$scope.showingCopyBox = false;
}, 10);
};
}
};
return directiveDefinitionObject;
});

View file

@ -1,16 +1,16 @@
/**
* An element which displays the features of an image.
* An element which displays the features of a manifest.
*/
angular.module('quay').directive('imageFeatureView', function () {
angular.module('quay').directive('manifestFeatureView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/image-feature-view.html',
templateUrl: '/static/directives/manifest-feature-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'image': '=image',
'manifest': '=manifest',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, TableService) {
@ -64,15 +64,15 @@ angular.module('quay').directive('imageFeatureView', function () {
});
};
var loadImageVulnerabilities = function() {
var loadManifestVulnerabilities = function() {
if ($scope.loading) {
return;
}
$scope.loading = true;
VulnerabilityService.loadImageVulnerabilities($scope.repository, $scope.image.id, function(resp) {
VulnerabilityService.loadManifestVulnerabilities($scope.repository, $scope.manifest.digest, function(resp) {
$scope.securityStatus = resp.status;
$scope.featuresInfo = VulnerabilityService.buildFeaturesInfo($scope.image, resp);
$scope.featuresInfo = VulnerabilityService.buildFeaturesInfo($scope.manifest.image, resp);
buildOrderedFeatures();
buildChart();
@ -87,20 +87,20 @@ angular.module('quay').directive('imageFeatureView', function () {
$scope.$watch('options.filter', buildOrderedFeatures);
$scope.$watch('repository', function(repository) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadManifestVulnerabilities();
}
});
$scope.$watch('image', function(image) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
$scope.$watch('manifest', function(manifest) {
if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadManifestVulnerabilities();
}
});
$scope.$watch('isEnabled', function(isEnabled) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadManifestVulnerabilities();
}
});
}

View file

@ -0,0 +1,37 @@
<span class="manifest-link">
<span class="id-label" ng-if="::!$ctrl.hasSHA256($ctrl.manifestDigest)"
data-title="The Docker V1 ID for this image. This ID is not content addressable nor is it stable across pulls."
data-container="body"
ng-click="$ctrl.showCopyBox()"
bs-tooltip>V1ID</span>
<span class="id-label cas" ng-if="::$ctrl.hasSHA256($ctrl.manifestDigest)"
data-title="The content-addressable SHA256 hash of this tag."
data-container="body"
ng-click="$ctrl.showCopyBox()"
bs-tooltip>SHA256</span>
<a ng-href="/repository/{{ ::$ctrl.repository.namespace }}/{{ ::$ctrl.repository.name }}/manifest/{{ ::$ctrl.manifestDigest }}">
{{ $ctrl.getShortDigest($ctrl.manifestDigest) }}
</a>
<span ng-if="::!$ctrl.hasSHA256($ctrl.manifestDigest)">{{ ::$ctrl.imageId.substr(0, 12) }}</span>
<div class="modal fade co-dialog" ng-if="$ctrl.showingCopyBox">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="$ctrl.hideCopyBox()"
aria-hidden="true">&times;</button>
<h4 class="modal-title"><span ng-if="$ctrl.hasSHA256($ctrl.manifestDigest)">Manifest SHA256</span><span ng-if="!$ctrl.hasSHA256($ctrl.manifestDigest)">V1 ID</span></h4>
</div>
<div class="modal-body">
<div class="copy-box" hovering-message="true" value="$ctrl.hasSHA256($ctrl.manifestDigest) ? $ctrl.manifestDigest : $ctrl.imageId"></div>
</div>
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-default" ng-click="$ctrl.hideCopyBox()">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</span>

View file

@ -0,0 +1,49 @@
import { Input, Component, Inject } from 'ng-metadata/core';
import { Repository } from '../../../types/common.types';
/**
* A component that links to a manifest view.
*/
@Component({
selector: 'manifest-link',
templateUrl: '/static/js/directives/ui/manifest-link/manifest-link.component.html'
})
export class ManifestLinkComponent {
@Input('<') public repository: Repository;
@Input('<') public manifestDigest: string;
@Input('<') public imageId: string;
private showingCopyBox: boolean = false;
constructor(@Inject('$timeout') private $timeout, @Inject('$element') private $element) {
}
private hasSHA256(digest: string) {
return digest && digest.indexOf('sha256:') == 0;
}
private getShortDigest(digest: string) {
if (!digest) { return ''; }
return digest.substr('sha256:'.length).substr(0, 12);
}
private showCopyBox() {
this.showingCopyBox = true;
// Necessary to wait for digest cycle to complete.
this.$timeout(() => {
this.$element.find('.modal').modal('show');
}, 10);
};
private hideCopyBox() {
this.$element.find('.modal').modal('hide');
// Wait for the modal to hide before removing from the DOM.
this.$timeout(() => {
this.showingCopyBox = false;
}, 10);
};
}

View file

@ -1,16 +1,16 @@
/**
* An element which displays the vulnerabilities in an image.
* An element which displays the vulnerabilities in a manifest.
*/
angular.module('quay').directive('imageVulnerabilityView', function () {
angular.module('quay').directive('manifestVulnerabilityView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/image-vulnerability-view.html',
templateUrl: '/static/directives/manifest-vulnerability-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'repository': '=repository',
'image': '=image',
'manifest': '=manifest',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, $routeParams, Config, ApiService, VulnerabilityService, ViewArray, TableService) {
@ -100,15 +100,15 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
});
};
var loadImageVulnerabilities = function() {
var loadManifestVulnerabilities = function() {
if ($scope.loading) {
return;
}
$scope.loading = true;
VulnerabilityService.loadImageVulnerabilities($scope.repository, $scope.image.id, function(resp) {
VulnerabilityService.loadManifestVulnerabilities($scope.repository, $scope.manifest.digest, function(resp) {
$scope.securityStatus = resp.status;
$scope.vulnerabilitiesInfo = VulnerabilityService.buildVulnerabilitiesInfo($scope.image, resp);
$scope.vulnerabilitiesInfo = VulnerabilityService.buildVulnerabilitiesInfo($scope.manifest.image, resp);
buildOrderedVulnerabilities();
buildChart();
@ -124,20 +124,20 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
$scope.$watch('options.fixableVulns', buildOrderedVulnerabilities);
$scope.$watch('repository', function(repository) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadManifestVulnerabilities();
}
});
$scope.$watch('image', function(image) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
$scope.$watch('manifest', function(manifest) {
if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadManifestVulnerabilities();
}
});
$scope.$watch('isEnabled', function(isEnabled) {
if ($scope.isEnabled && $scope.repository && $scope.image) {
loadImageVulnerabilities();
if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadManifestVulnerabilities();
}
});
}

View file

@ -1,69 +0,0 @@
(function() {
/**
* Page to view the details of a single image.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('image-view', 'image-view.html', ImageViewCtrl, {
'newLayout': true,
'title': '{{ image.id }}',
'description': 'Image {{ image.id }}'
})
}]);
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService, Features, CookieService) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var imageid = $routeParams.image;
$scope.imageSecurityCounter = 0;
$scope.imagePackageCounter = 0;
$scope.options = {
'vulnFilter': ''
};
var loadImage = function() {
var params = {
'repository': namespace + '/' + name,
'image_id': imageid
};
$scope.imageResource = ApiService.getImageAsResource(params).get(function(image) {
$scope.image = image;
$scope.reversedHistory = image.history.reverse();
});
};
var loadRepository = function() {
var params = {
'repository': namespace + '/' + name
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repository = repo;
});
};
loadImage();
loadRepository();
$scope.loadImageSecurity = function() {
if (!Features.SECURITY_SCANNER) { return; }
$scope.imageSecurityCounter++;
};
$scope.loadImagePackages = function() {
if (!Features.SECURITY_SCANNER) { return; }
$scope.imagePackageCounter++;
};
$scope.initializeTree = function() {
if ($scope.tree || !$scope.combinedChanges.length) { return; }
$scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges);
$timeout(function() {
$scope.tree.draw('changes-tree-container');
}, 100);
};
}
})();

View file

@ -0,0 +1,60 @@
(function() {
/**
* Page to view the details of a single manifest.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('manifest-view', 'manifest-view.html', ManifestViewCtrl, {
'newLayout': true,
'title': '{{ manifest_digest }}',
'description': 'Manifest {{ manifest_digest }}'
})
}]);
function ManifestViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService, Features, CookieService) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var manifest_digest = $routeParams.manifest_digest;
$scope.manifestSecurityCounter = 0;
$scope.manifestPackageCounter = 0;
$scope.options = {
'vulnFilter': ''
};
var loadManifest = function() {
var params = {
'repository': namespace + '/' + name,
'manifestref': manifest_digest
};
$scope.manifestResource = ApiService.getRepoManifestAsResource(params).get(function(manifest) {
$scope.manifest = manifest;
$scope.reversedHistory = manifest.image.history.reverse();
});
};
var loadRepository = function() {
var params = {
'repository': namespace + '/' + name
};
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repository = repo;
});
};
loadManifest();
loadRepository();
$scope.loadManifestSecurity = function() {
if (!Features.SECURITY_SCANNER) { return; }
$scope.manifestSecurityCounter++;
};
$scope.loadManifestPackages = function() {
if (!Features.SECURITY_SCANNER) { return; }
$scope.manifestPackageCounter++;
};
}
})();

View file

@ -68,7 +68,7 @@ function provideRoutes($routeProvider: ng.route.IRouteProvider,
.route('/repository/:namespace/:name/tag/:tag', 'repo-view')
// Image View
.route('/repository/:namespace/:name/image/:image', 'image-view')
.route('/repository/:namespace/:name/manifest/:manifest_digest', 'manifest-view')
// Repo Build View
.route('/repository/:namespace/:name/build/:buildid', 'build-view')

View file

@ -40,6 +40,7 @@ import { TriggerDescriptionComponent } from './directives/ui/trigger-description
import { TimeAgoComponent } from './directives/ui/time-ago/time-ago.component';
import { TimeDisplayComponent } from './directives/ui/time-display/time-display.component';
import { AppSpecificTokenManagerComponent } from './directives/ui/app-specific-token-manager/app-specific-token-manager.component';
import { ManifestLinkComponent } from './directives/ui/manifest-link/manifest-link.component';
import { MarkdownModule } from './directives/ui/markdown/markdown.module';
import * as Clipboard from 'clipboard';
@ -85,6 +86,7 @@ import * as Clipboard from 'clipboard';
TimeAgoComponent,
TimeDisplayComponent,
AppSpecificTokenManagerComponent,
ManifestLinkComponent,
],
providers: [
ViewArrayImpl,

View file

@ -286,16 +286,6 @@ angular.module('quay').factory('VulnerabilityService', ['Config', 'ApiService',
}
};
vulnService.loadImageVulnerabilitiesAsResource = function(repo, image_id, result) {
var params = {
'repository': repo.namespace + '/' + repo.name,
'imageid': image_id,
'vulnerabilities': true,
};
return ApiService.getRepoImageSecurityAsResource(params).get(result);
};
vulnService.loadImageVulnerabilities = function(repo, image_id, result, reject) {
var params = {
'imageid': image_id,
@ -306,6 +296,16 @@ angular.module('quay').factory('VulnerabilityService', ['Config', 'ApiService',
ApiService.getRepoImageSecurity(null, params).then(result, reject);
};
vulnService.loadManifestVulnerabilities = function(repo, digest, result, reject) {
var params = {
'manifestref': digest,
'repository': repo.namespace + '/' + repo.name,
'vulnerabilities': true,
};
ApiService.getRepoManifestSecurity(null, params).then(result, reject);
};
vulnService.hasFeatures = function(resp) {
return resp.data && resp.data.Layer && resp.data.Layer.Features && resp.data.Layer.Features.length;
};

View file

@ -1,6 +1,6 @@
<div class="resource-view image-view"
resources="[repositoryResource, imageResource]"
error-message="'Image not found'">
<div class="resource-view manifest-view"
resources="[repositoryResource, manifestResource]"
error-message="'Manifest not found'">
<div class="page-content">
<div class="cor-title">
<span class="cor-title-link">
@ -10,8 +10,8 @@
</a>
</span>
<span class="cor-title-content">
<i class="fa fa-archive fa-lg" style="margin-right: 10px"></i>
{{ image.id.substr(0, 12) }}
<i class="fa fa-file fa-lg" style="margin-right: 10px"></i>
{{ manifest.digest.substr(7, 12) }}
</span>
</div>
@ -21,12 +21,12 @@
<i class="fa ci-layers"></i>
</cor-tab>
<cor-tab tab-title="Security Scan" tab-id="vulnerabilities"
tab-init="loadImageSecurity()"
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="loadImagePackages()"
tab-init="loadManifestPackages()"
quay-show="Features.SECURITY_SCANNER">
<i class="fa ci-package"></i>
</cor-tab>
@ -35,23 +35,23 @@
<cor-tab-content>
<!-- Layers -->
<cor-tab-pane id="layers">
<h3>Image Layers</h3>
<div class="image-view-layer" repository="repository" image="image" images="image.history"></div>
<div class="image-view-layer" repository="repository" image="parent" images="image.history"
<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>
<!-- Vulnerabilities -->
<cor-tab-pane id="vulnerabilities" quay-show="Features.SECURITY_SCANNER">
<div quay-require="['SECURITY_SCANNER']">
<div class="image-vulnerability-view" repository="repository" image="image" is-enabled="imageSecurityCounter"></div>
<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="image-feature-view" repository="repository" image="image" is-enabled="imagePackageCounter"></div>
<div class="manifest-feature-view" repository="repository" manifest="manifest" is-enabled="manifestPackageCounter"></div>
</div>
</cor-tab-pane>
</cor-tab-content>