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:
commit
0c1b13828f
30 changed files with 428 additions and 379 deletions
|
@ -1,4 +1,5 @@
|
|||
""" Manage the manifests of a repository. """
|
||||
import json
|
||||
|
||||
from app import label_validator
|
||||
from flask import request
|
||||
|
@ -18,6 +19,22 @@ MANIFEST_DIGEST_ROUTE = BASE_MANIFEST_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
|||
ALLOWED_LABEL_MEDIA_TYPES = ['text/plain', 'application/json']
|
||||
|
||||
|
||||
@resource(MANIFEST_DIGEST_ROUTE)
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('manifestref', 'The digest of the manifest')
|
||||
class RepositoryManifest(RepositoryParamResource):
|
||||
""" Resource for retrieving a specific repository manifest. """
|
||||
@require_repo_read
|
||||
@nickname('getRepoManifest')
|
||||
@disallow_for_app_repositories
|
||||
def get(self, namespace_name, repository_name, manifestref):
|
||||
manifest = model.get_repository_manifest(namespace_name, repository_name, manifestref)
|
||||
if manifest is None:
|
||||
raise NotFound()
|
||||
|
||||
return manifest.to_dict()
|
||||
|
||||
|
||||
@resource(MANIFEST_DIGEST_ROUTE + '/labels')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('manifestref', 'The digest of the manifest')
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from manifest_models_interface import ManifestLabel, ManifestLabelInterface
|
||||
import json
|
||||
|
||||
from manifest_models_interface import ManifestLabel, ManifestLabelInterface, ManifestAndImage
|
||||
from data import model
|
||||
from image_models_pre_oci import pre_oci_model as image_models
|
||||
|
||||
|
||||
class ManifestLabelPreOCI(ManifestLabelInterface):
|
||||
|
@ -36,6 +39,20 @@ class ManifestLabelPreOCI(ManifestLabelInterface):
|
|||
|
||||
return self._label(model.label.delete_manifest_label(label_uuid, tag_manifest))
|
||||
|
||||
def get_repository_manifest(self, namespace_name, repository_name, digest):
|
||||
try:
|
||||
tag_manifest = model.tag.load_manifest_by_digest(namespace_name, repository_name, digest)
|
||||
except model.DataModelException:
|
||||
return None
|
||||
|
||||
# TODO: remove this dependency on image once we've moved to the new data model.
|
||||
image = image_models.get_repository_image(namespace_name, repository_name,
|
||||
tag_manifest.tag.image.docker_image_id)
|
||||
|
||||
manifest_data = json.loads(tag_manifest.json_data)
|
||||
return ManifestAndImage(digest=digest, manifest_data=manifest_data, image=image)
|
||||
|
||||
|
||||
def _label(self, label_obj):
|
||||
if not label_obj:
|
||||
return None
|
||||
|
|
22
endpoints/api/test/test_manifest.py
Normal file
22
endpoints/api/test/test_manifest.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
import pytest
|
||||
|
||||
from data import model
|
||||
from endpoints.api.manifest import RepositoryManifest
|
||||
from endpoints.api.test.shared import conduct_api_call
|
||||
from endpoints.test.shared import client_with_identity
|
||||
from test.fixtures import *
|
||||
|
||||
def test_repository_manifest(client):
|
||||
with client_with_identity('devtable', client) as cl:
|
||||
tags = model.tag.list_repository_tags('devtable', 'simple')
|
||||
digests = model.tag.get_tag_manifest_digests(tags)
|
||||
for tag in tags:
|
||||
manifest = digests[tag.id]
|
||||
params = {
|
||||
'repository': 'devtable/simple',
|
||||
'manifestref': manifest,
|
||||
}
|
||||
result = conduct_api_call(cl, RepositoryManifest, 'GET', params, None, 200).json
|
||||
assert result['digest'] == manifest
|
||||
assert result['manifest_data']
|
||||
assert result['image']
|
|
@ -15,6 +15,7 @@ from endpoints.api.search import ConductRepositorySearch
|
|||
from endpoints.api.superuser import SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource
|
||||
from endpoints.api.superuser import SuperUserRepositoryBuildStatus
|
||||
from endpoints.api.appspecifictokens import AppTokens, AppToken
|
||||
from endpoints.api.manifest import RepositoryManifest
|
||||
from endpoints.api.trigger import BuildTrigger
|
||||
from endpoints.test.shared import client_with_identity, toggle_feature
|
||||
|
||||
|
@ -28,6 +29,7 @@ SEARCH_PARAMS = {'query': ''}
|
|||
NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', 'uuid': 'some uuid'}
|
||||
TOKEN_PARAMS = {'token_uuid': 'someuuid'}
|
||||
TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_uuid': 'someuuid'}
|
||||
MANIFEST_PARAMS = {'repository': 'devtable/simple', 'manifestref': 'sha256:deadbeef'}
|
||||
|
||||
@pytest.mark.parametrize('resource,method,params,body,identity,expected', [
|
||||
(AppTokens, 'GET', {}, {}, None, 401),
|
||||
|
@ -50,6 +52,11 @@ TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_uuid': 'someuuid'}
|
|||
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'reader', 404),
|
||||
(AppToken, 'DELETE', TOKEN_PARAMS, {}, 'devtable', 404),
|
||||
|
||||
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, None, 401),
|
||||
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, 'freshuser', 403),
|
||||
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, 'reader', 403),
|
||||
(RepositoryManifest, 'GET', MANIFEST_PARAMS, {}, 'devtable', 404),
|
||||
|
||||
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, None, 401),
|
||||
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'freshuser', 403),
|
||||
(OrganizationCollaboratorList, 'GET', ORG_PARAMS, None, 'reader', 403),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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">×</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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">·</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 -->
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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">×</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>
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
})();
|
60
static/js/pages/manifest-view.js
Normal file
60
static/js/pages/manifest-view.js
Normal 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++;
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
Reference in a new issue