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. """ """ Manage the manifests of a repository. """
import json
from app import label_validator from app import label_validator
from flask import request 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'] 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') @resource(MANIFEST_DIGEST_ROUTE + '/labels')
@path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('manifestref', 'The digest of the manifest') @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) @add_metaclass(ABCMeta)
class ManifestLabelInterface(object): class ManifestLabelInterface(object):
""" """
@ -95,3 +109,9 @@ class ManifestLabelInterface(object):
Returns: Returns:
ManifestLabel or None 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 data import model
from image_models_pre_oci import pre_oci_model as image_models
class ManifestLabelPreOCI(ManifestLabelInterface): class ManifestLabelPreOCI(ManifestLabelInterface):
@ -36,6 +39,20 @@ class ManifestLabelPreOCI(ManifestLabelInterface):
return self._label(model.label.delete_manifest_label(label_uuid, tag_manifest)) 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): def _label(self, label_obj):
if not label_obj: if not label_obj:
return None 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 SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource
from endpoints.api.superuser import SuperUserRepositoryBuildStatus from endpoints.api.superuser import SuperUserRepositoryBuildStatus
from endpoints.api.appspecifictokens import AppTokens, AppToken from endpoints.api.appspecifictokens import AppTokens, AppToken
from endpoints.api.manifest import RepositoryManifest
from endpoints.api.trigger import BuildTrigger from endpoints.api.trigger import BuildTrigger
from endpoints.test.shared import client_with_identity, toggle_feature 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'} NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', 'uuid': 'some uuid'}
TOKEN_PARAMS = {'token_uuid': 'someuuid'} TOKEN_PARAMS = {'token_uuid': 'someuuid'}
TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_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', [ @pytest.mark.parametrize('resource,method,params,body,identity,expected', [
(AppTokens, 'GET', {}, {}, None, 401), (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, {}, 'reader', 404),
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'devtable', 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, None, 401),
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'freshuser', 403), (OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'freshuser', 403),
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'reader', 403), (OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'reader', 403),

View file

@ -1,29 +1,7 @@
.image-view-layer-element { .image-view-layer-element {
position: relative; position: relative;
padding: 10px; padding: 10px;
padding-left: 170px; padding-left: 40px;
}
.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;
} }
.image-view-layer-element .image-comment { .image-view-layer-element .image-comment {
@ -47,7 +25,7 @@
position: absolute; position: absolute;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
left: 140px; left: 10px;
border-left: 2px solid #428bca; border-left: 2px solid #428bca;
width: 0px; width: 0px;
@ -64,7 +42,7 @@
.image-view-layer-element .image-layer-dot { .image-view-layer-element .image-layer-dot {
position: absolute; position: absolute;
top: 14px; top: 14px;
left: 135px; left: 5px;
border: 2px solid #428bca; border: 2px solid #428bca;
border-radius: 50%; border-radius: 50%;
width: 12px; width: 12px;
@ -79,14 +57,6 @@
} }
@media (max-width: 767px) { @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 { .image-view-layer-element .dockerfile-command-element .label {
position: relative; position: relative;
display: block; display: block;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,70 +1,70 @@
.image-view .image-view-header { .manifest-view .manifest-view-header {
padding: 10px; padding: 10px;
background: #e8f1f6; background: #e8f1f6;
margin: -10px; margin: -10px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.image-view .image-view-header .section-icon { .manifest-view .manifest-view-header .section-icon {
margin-right: 6px; margin-right: 6px;
} }
.image-view .image-view-header .section { .manifest-view .manifest-view-header .section {
padding: 4px; padding: 4px;
display: inline-block; display: inline-block;
margin-right: 20px; margin-right: 20px;
} }
.image-view .co-tab-content { .manifest-view .co-tab-content {
padding: 20px; padding: 20px;
padding-top: 10px; padding-top: 10px;
} }
.image-view .co-tab-content h3 { .manifest-view .co-tab-content h3 {
margin-bottom: 30px; margin-bottom: 30px;
} }
.image-view .fa-bug { .manifest-view .fa-bug {
margin-right: 4px; margin-right: 4px;
} }
.image-view .co-filter-box { .manifest-view .co-filter-box {
float: right; float: right;
min-width: 300px; min-width: 300px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.image-view .co-filter-box .current-filtered { .manifest-view .co-filter-box .current-filtered {
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
color: #999; color: #999;
} }
.image-view .co-filter-box input { .manifest-view .co-filter-box input {
display: inline-block; display: inline-block;
} }
.image-view .level-col h4 { .manifest-view .level-col h4 {
margin-top: 0px; margin-top: 0px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.image-view .levels { .manifest-view .levels {
list-style: none; list-style: none;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
} }
.image-view .levels li { .manifest-view .levels li {
margin-bottom: 20px; margin-bottom: 20px;
} }
.image-view .levels li .description { .manifest-view .levels li .description {
margin-top: 6px; margin-top: 6px;
font-size: 14px; font-size: 14px;
color: #999; color: #999;
} }
.image-view .level-col { .manifest-view .level-col {
padding: 20px; 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,9 +1,4 @@
<div class="image-view-layer-element" ng-class="getClass()"> <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> <image-command command="image.command"></image-command>
</div> </div>

View file

@ -1,4 +1,4 @@
<div class="image-feature-view-element"> <div class="manifest-feature-view-element">
<!-- Unable to load --> <!-- Unable to load -->
<div class="empty" ng-if="securityStatus == 'error'"> <div class="empty" ng-if="securityStatus == 'error'">
<div class="empty-icon"> <div class="empty-icon">
@ -15,7 +15,7 @@
<div class="empty-icon"> <div class="empty-icon">
<i class="fa fa-ellipsis-h"></i> <i class="fa fa-ellipsis-h"></i>
</div> </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"> <div class="empty-secondary-msg">
Please try again in a few minutes. Please try again in a few minutes.
</div> </div>
@ -26,9 +26,9 @@
<div class="empty-icon"> <div class="empty-icon">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
</div> </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"> <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>
</div> </div>
@ -38,9 +38,9 @@
<div class="empty-icon"> <div class="empty-icon">
<i class="fa ci-package"></i> <i class="fa ci-package"></i>
</div> </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"> <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> </div>
</div> </div>
@ -79,7 +79,7 @@
</span> </span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Packages..."> <input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Packages...">
</span> </span>
<h3>Image Packages</h3> <h3>Packages</h3>
<!-- Table --> <!-- Table -->
<table class="co-table"> <table class="co-table">
@ -165,11 +165,8 @@
<td class="double-col image-col hidden-xs hidden-sm hidden-md"> <td class="double-col image-col hidden-xs hidden-sm hidden-md">
<span bo-if="feature.imageCommand"> <span bo-if="feature.imageCommand">
<image-command command="feature.imageCommand"></image-command> <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>
<span bo-if="!feature.imageCommand">(No Command)</span>
</td> </td>
<td></td> <td></td>
</tr> </tr>

View file

@ -1,4 +1,4 @@
<div class="image-vulnerability-view-element"> <div class="manifest-vulnerability-view-element">
<!-- Unable to load --> <!-- Unable to load -->
<div class="empty" ng-if="securityStatus == 'error'"> <div class="empty" ng-if="securityStatus == 'error'">
<div class="empty-icon"> <div class="empty-icon">
@ -15,7 +15,7 @@
<div class="empty-icon"> <div class="empty-icon">
<i class="fa fa-ellipsis-h"></i> <i class="fa fa-ellipsis-h"></i>
</div> </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"> <div class="empty-secondary-msg">
Please try again in a few minutes. Please try again in a few minutes.
</div> </div>
@ -26,9 +26,9 @@
<div class="empty-icon"> <div class="empty-icon">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
</div> </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"> <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>
</div> </div>
@ -38,9 +38,9 @@
<div class="empty-icon"> <div class="empty-icon">
<i class="fa fa-bug"></i> <i class="fa fa-bug"></i>
</div> </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"> <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> </div>
</div> </div>
@ -72,7 +72,7 @@
</ul> </ul>
<div ng-if="!vulnerabilitiesInfo.severityBreakdown.length"> <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> </div>
</div> </div>
@ -87,13 +87,13 @@
<label><input type="checkbox" ng-model="options.fixableVulns">Only show fixable</label> <label><input type="checkbox" ng-model="options.fixableVulns">Only show fixable</label>
</div> </div>
</span> </span>
<h3>Image Vulnerabilities</h3> <h3>Vulnerabilities</h3>
<!-- Table --> <!-- Table -->
<div class="empty" ng-if="!vulnerabilitiesInfo.vulnerabilities.length" <div class="empty" ng-if="!vulnerabilitiesInfo.vulnerabilities.length"
style="margin-top: 20px;"> style="margin-top: 20px;">
<div class="empty-primary-msg">No vulnerabilities found.</div> <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> </div>
<table class="co-table" ng-show="vulnerabilitiesInfo.vulnerabilities.length"> <table class="co-table" ng-show="vulnerabilitiesInfo.vulnerabilities.length">
@ -111,7 +111,7 @@
<td class="hidden-xs">Current version</td> <td class="hidden-xs">Current version</td>
<td class="hidden-xs hidden-sm">Fixed in version</td> <td class="hidden-xs hidden-sm">Fixed in version</td>
</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> <td class="hidden-xs options-col"></td>
</thead> </thead>
<tbody ng-repeat="vuln in orderedVulnerabilities.visibleEntries" bindonce> <tbody ng-repeat="vuln in orderedVulnerabilities.visibleEntries" bindonce>
@ -152,12 +152,9 @@
</td> </td>
<td class="double-col image-col hidden-xs hidden-sm hidden-md"> <td class="double-col image-col hidden-xs hidden-sm hidden-md">
<span bo-if="vuln.imageCommand"> <span bo-if="vuln.imageCommand">
<image-command command="vuln.imageCommand"></image-command> <image-command command="feature.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>
</span> </span>
<span bo-if="!vuln.imageCommand">(No Command)</span>
</td> </td>
<td></td> <td></td>
</tr> </tr>
@ -181,8 +178,8 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Introduced in Image:</td> <td>Introduced in Layer:</td>
<td><span class="image-link" repository="repository" image-id="vuln.imageId"></span></td> <td>{{ ::vuln.imageId }}</td>
</tr> </tr>
</table> </table>
</div> </div>

View file

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

View file

@ -161,6 +161,14 @@
</span> </span>
<span ng-if="!getTagVulnerabilities(tag).loading"> <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 --> <!-- Queued -->
<span class="scanning" ng-if="getTagVulnerabilities(tag).status == 'queued'" <span class="scanning" ng-if="getTagVulnerabilities(tag).status == 'queued'"
data-title="The image for this tag is queued to be scanned for vulnerabilities" 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" data-title="The image for this tag has no vulnerabilities as found in our database"
bs-tooltip bs-tooltip
bindonce> 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> <span class="donut-chart" width="22" data="[{'index': 0, 'value': 1, 'color': '#2FC98E'}]"></span>
Passed Passed
</a> </a>
@ -204,7 +212,7 @@
ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority" ng-class="getTagVulnerabilities(tag).highestVulnerability.Priority"
class="has-vulns" bindonce> 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" data-title="This tag has {{ getTagVulnerabilities(tag).vulnerabilities.length }} vulnerabilities across {{ getTagVulnerabilities(tag).featuresInfo.brokenFeaturesCount }} packages"
bs-tooltip> bs-tooltip>
<!-- Donut --> <!-- Donut -->
@ -218,7 +226,7 @@
</span> </span>
</a> </a>
<span class="dot" ng-if="getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length">&middot;</span> <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 {{ getTagVulnerabilities(tag).vulnerabilitiesInfo.fixable.length }} fixable
</a> </a>
</span> </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> <expiration-status-view expiration-date="tag.expiration_date" ng-if="repository.tag_operations_disabled || !repository.can_write"></expiration-status-view>
</td> </td>
<!-- Image link --> <!-- Manifest link -->
<td class="hidden-xs hidden-sm image-id-col"> <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>
<td class="hidden-xs hidden-sm hidden-md image-track" <td class="hidden-xs hidden-sm hidden-md image-track"
ng-if="imageTracks.length > maxTrackCount" bindonce> 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) }}"> <td class="labels-col" colspan="{{6 + (repository.trust_enabled ? 1 : 0) + (Features.SECURITY_SCANNER ? 1 : 0) }}">
<!-- Image ID --> <!-- Image ID -->
<div class="image-id-row"> <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> </div>
<!-- Labels --> <!-- Labels -->

View file

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

View file

@ -262,6 +262,10 @@ angular.module('quay').directive('repoPanelTags', function () {
}; };
$scope.getTagVulnerabilities = function(tag) { $scope.getTagVulnerabilities = function(tag) {
if (!tag.manifest_digest) {
return 'nodigest';
}
return $scope.getImageVulnerabilities(tag.image_id); 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 = { var directiveDefinitionObject = {
priority: 0, priority: 0,
templateUrl: '/static/directives/image-feature-view.html', templateUrl: '/static/directives/manifest-feature-view.html',
replace: false, replace: false,
transclude: true, transclude: true,
restrict: 'C', restrict: 'C',
scope: { scope: {
'repository': '=repository', 'repository': '=repository',
'image': '=image', 'manifest': '=manifest',
'isEnabled': '=isEnabled' 'isEnabled': '=isEnabled'
}, },
controller: function($scope, $element, Config, ApiService, VulnerabilityService, ViewArray, TableService) { 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) { if ($scope.loading) {
return; return;
} }
$scope.loading = true; $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.securityStatus = resp.status;
$scope.featuresInfo = VulnerabilityService.buildFeaturesInfo($scope.image, resp); $scope.featuresInfo = VulnerabilityService.buildFeaturesInfo($scope.manifest.image, resp);
buildOrderedFeatures(); buildOrderedFeatures();
buildChart(); buildChart();
@ -87,20 +87,20 @@ angular.module('quay').directive('imageFeatureView', function () {
$scope.$watch('options.filter', buildOrderedFeatures); $scope.$watch('options.filter', buildOrderedFeatures);
$scope.$watch('repository', function(repository) { $scope.$watch('repository', function(repository) {
if ($scope.isEnabled && $scope.repository && $scope.image) { if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadImageVulnerabilities(); loadManifestVulnerabilities();
} }
}); });
$scope.$watch('image', function(image) { $scope.$watch('manifest', function(manifest) {
if ($scope.isEnabled && $scope.repository && $scope.image) { if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadImageVulnerabilities(); loadManifestVulnerabilities();
} }
}); });
$scope.$watch('isEnabled', function(isEnabled) { $scope.$watch('isEnabled', function(isEnabled) {
if ($scope.isEnabled && $scope.repository && $scope.image) { if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadImageVulnerabilities(); 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 = { var directiveDefinitionObject = {
priority: 0, priority: 0,
templateUrl: '/static/directives/image-vulnerability-view.html', templateUrl: '/static/directives/manifest-vulnerability-view.html',
replace: false, replace: false,
transclude: true, transclude: true,
restrict: 'C', restrict: 'C',
scope: { scope: {
'repository': '=repository', 'repository': '=repository',
'image': '=image', 'manifest': '=manifest',
'isEnabled': '=isEnabled' 'isEnabled': '=isEnabled'
}, },
controller: function($scope, $element, $routeParams, Config, ApiService, VulnerabilityService, ViewArray, TableService) { 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) { if ($scope.loading) {
return; return;
} }
$scope.loading = true; $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.securityStatus = resp.status;
$scope.vulnerabilitiesInfo = VulnerabilityService.buildVulnerabilitiesInfo($scope.image, resp); $scope.vulnerabilitiesInfo = VulnerabilityService.buildVulnerabilitiesInfo($scope.manifest.image, resp);
buildOrderedVulnerabilities(); buildOrderedVulnerabilities();
buildChart(); buildChart();
@ -124,20 +124,20 @@ angular.module('quay').directive('imageVulnerabilityView', function () {
$scope.$watch('options.fixableVulns', buildOrderedVulnerabilities); $scope.$watch('options.fixableVulns', buildOrderedVulnerabilities);
$scope.$watch('repository', function(repository) { $scope.$watch('repository', function(repository) {
if ($scope.isEnabled && $scope.repository && $scope.image) { if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadImageVulnerabilities(); loadManifestVulnerabilities();
} }
}); });
$scope.$watch('image', function(image) { $scope.$watch('manifest', function(manifest) {
if ($scope.isEnabled && $scope.repository && $scope.image) { if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadImageVulnerabilities(); loadManifestVulnerabilities();
} }
}); });
$scope.$watch('isEnabled', function(isEnabled) { $scope.$watch('isEnabled', function(isEnabled) {
if ($scope.isEnabled && $scope.repository && $scope.image) { if ($scope.isEnabled && $scope.repository && $scope.manifest) {
loadImageVulnerabilities(); 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') .route('/repository/:namespace/:name/tag/:tag', 'repo-view')
// Image View // Image View
.route('/repository/:namespace/:name/image/:image', 'image-view') .route('/repository/:namespace/:name/manifest/:manifest_digest', 'manifest-view')
// Repo Build View // Repo Build View
.route('/repository/:namespace/:name/build/:buildid', '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 { TimeAgoComponent } from './directives/ui/time-ago/time-ago.component';
import { TimeDisplayComponent } from './directives/ui/time-display/time-display.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 { 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 { MarkdownModule } from './directives/ui/markdown/markdown.module';
import * as Clipboard from 'clipboard'; import * as Clipboard from 'clipboard';
@ -85,6 +86,7 @@ import * as Clipboard from 'clipboard';
TimeAgoComponent, TimeAgoComponent,
TimeDisplayComponent, TimeDisplayComponent,
AppSpecificTokenManagerComponent, AppSpecificTokenManagerComponent,
ManifestLinkComponent,
], ],
providers: [ providers: [
ViewArrayImpl, 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) { vulnService.loadImageVulnerabilities = function(repo, image_id, result, reject) {
var params = { var params = {
'imageid': image_id, 'imageid': image_id,
@ -306,6 +296,16 @@ angular.module('quay').factory('VulnerabilityService', ['Config', 'ApiService',
ApiService.getRepoImageSecurity(null, params).then(result, reject); 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) { vulnService.hasFeatures = function(resp) {
return resp.data && resp.data.Layer && resp.data.Layer.Features && resp.data.Layer.Features.length; 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" <div class="resource-view manifest-view"
resources="[repositoryResource, imageResource]" resources="[repositoryResource, manifestResource]"
error-message="'Image not found'"> error-message="'Manifest not found'">
<div class="page-content"> <div class="page-content">
<div class="cor-title"> <div class="cor-title">
<span class="cor-title-link"> <span class="cor-title-link">
@ -10,8 +10,8 @@
</a> </a>
</span> </span>
<span class="cor-title-content"> <span class="cor-title-content">
<i class="fa fa-archive fa-lg" style="margin-right: 10px"></i> <i class="fa fa-file fa-lg" style="margin-right: 10px"></i>
{{ image.id.substr(0, 12) }} {{ manifest.digest.substr(7, 12) }}
</span> </span>
</div> </div>
@ -21,12 +21,12 @@
<i class="fa ci-layers"></i> <i class="fa ci-layers"></i>
</cor-tab> </cor-tab>
<cor-tab tab-title="Security Scan" tab-id="vulnerabilities" <cor-tab tab-title="Security Scan" tab-id="vulnerabilities"
tab-init="loadImageSecurity()" tab-init="loadManifestSecurity()"
quay-show="Features.SECURITY_SCANNER"> quay-show="Features.SECURITY_SCANNER">
<i class="fa fa-bug"></i> <i class="fa fa-bug"></i>
</cor-tab> </cor-tab>
<cor-tab tab-title="Packages" tab-id="packages" <cor-tab tab-title="Packages" tab-id="packages"
tab-init="loadImagePackages()" tab-init="loadManifestPackages()"
quay-show="Features.SECURITY_SCANNER"> quay-show="Features.SECURITY_SCANNER">
<i class="fa ci-package"></i> <i class="fa ci-package"></i>
</cor-tab> </cor-tab>
@ -35,23 +35,23 @@
<cor-tab-content> <cor-tab-content>
<!-- Layers --> <!-- Layers -->
<cor-tab-pane id="layers"> <cor-tab-pane id="layers">
<h3>Image Layers</h3> <h3>Manifest Layers</h3>
<div class="image-view-layer" repository="repository" image="image" images="image.history"></div> <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="image.history" <div class="image-view-layer" repository="repository" image="parent" images="manifest.image.history"
ng-repeat="parent in reversedHistory"></div> ng-repeat="parent in reversedHistory"></div>
</cor-tab-pane> </cor-tab-pane>
<!-- Vulnerabilities --> <!-- Vulnerabilities -->
<cor-tab-pane id="vulnerabilities" 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="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> </div>
</cor-tab-pane> </cor-tab-pane>
<!-- Features --> <!-- Features -->
<cor-tab-pane id="packages" quay-show="Features.SECURITY_SCANNER"> <cor-tab-pane id="packages" quay-show="Features.SECURITY_SCANNER">
<div quay-require="['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> </div>
</cor-tab-pane> </cor-tab-pane>
</cor-tab-content> </cor-tab-content>