Switch from an image view UI to a manifest view UI
We no longer allow viewing individual images, but instead only manifests. This will help with the transition to Clair V3 (which is manifest based) and, eventually, the the new data model (which will also be manifest based)
This commit is contained in:
parent
d41dcaae23
commit
fc6eb71ab1
24 changed files with 312 additions and 260 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;
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
.manifest-link a {
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,9 +1,4 @@
|
|||
<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">
|
||||
<image-command command="image.command"></image-command>
|
||||
</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">
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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,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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
<span class="manifest-link">
|
||||
<span class="id-label" ng-if="!$ctrl.hasSHA256($ctrl.manifestDigest)"
|
||||
<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)"
|
||||
<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 bo-href-i="/repository/{{ $ctrl.repository.namespace }}/{{ $ctrl.repository.name }}/image/{{ $ctrl.imageId }}"
|
||||
class="image-link-element" bindonce>
|
||||
<span ng-if="!$ctrl.hasSHA256($ctrl.manifestDigest)">{{ $ctrl.imageId.substr(0, 12) }}</span>
|
||||
<span ng-if="$ctrl.hasSHA256($ctrl.manifestDigest)">{{ $ctrl.getShortDigest($ctrl.manifestDigest) }}</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">
|
||||
|
|
|
@ -25,6 +25,7 @@ export class ManifestLinkComponent {
|
|||
}
|
||||
|
||||
private getShortDigest(digest: string) {
|
||||
if (!digest) { return ''; }
|
||||
return digest.substr('sha256:'.length).substr(0, 12);
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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