mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-07 07:21:22 +01:00
Merge remote-tracking branch 'upstream/master' into 170608_project
This commit is contained in:
commit
aecfcef51a
@ -91,6 +91,7 @@ script:
|
||||
- ./tests/pushimage.sh
|
||||
- cd tests
|
||||
- sudo ./ldapprepare.sh
|
||||
- sudo ./admiral.sh
|
||||
- cd ..
|
||||
- go test -i ./src/ui ./src/adminserver ./src/jobservice
|
||||
- sudo -E env "PATH=$PATH" ./tests/coverage4gotest.sh
|
||||
|
@ -180,6 +180,20 @@ create table img_scan_job (
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
create table img_scan_overview (
|
||||
image_digest varchar(128) NOT NULL,
|
||||
scan_job_id int NOT NULL,
|
||||
/* 0 indicates none, the higher the number, the more severe the status */
|
||||
severity int NOT NULL default 0,
|
||||
/* the json string to store components severity status, currently use a json to be more flexible and avoid creating additional tables. */
|
||||
components_overview varchar(2048),
|
||||
/* primary key for querying details, in clair it should be the name of the "top layer" */
|
||||
details_key varchar(128),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(image_digest)
|
||||
);
|
||||
|
||||
create table properties (
|
||||
k varchar(64) NOT NULL,
|
||||
v varchar(128) NOT NULL,
|
||||
|
@ -171,6 +171,19 @@ create table img_scan_job (
|
||||
update_time timestamp default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
create table img_scan_overview (
|
||||
image_digest varchar(128) PRIMARY KEY,
|
||||
scan_job_id int NOT NULL,
|
||||
/* 0 indicates none, the higher the number, the more severe the status */
|
||||
severity int NOT NULL default 0,
|
||||
/* the json string to store components severity status, currently use a json to be more flexible and avoid creating additional tables. */
|
||||
components_overview varchar(2048),
|
||||
/* primary key for querying details, in clair it should be the name of the "top layer" */
|
||||
details_key varchar(128),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX policy ON replication_job (policy_id);
|
||||
CREATE INDEX poid_uptime ON replication_job (policy_id, update_time);
|
||||
|
||||
|
@ -16,7 +16,7 @@ clair:
|
||||
# Deadline before an API request will respond with a 503
|
||||
timeout: 300s
|
||||
updater:
|
||||
interval: 0h
|
||||
interval: 2h
|
||||
|
||||
notifier:
|
||||
attempts: 3
|
||||
|
@ -8,6 +8,9 @@ services:
|
||||
jobservice:
|
||||
networks:
|
||||
- harbor-clair
|
||||
registry:
|
||||
networks:
|
||||
- harbor-clair
|
||||
postgres:
|
||||
networks:
|
||||
harbor-clair:
|
||||
|
@ -28,9 +28,6 @@ const (
|
||||
RoleDeveloper = 2
|
||||
RoleGuest = 3
|
||||
|
||||
DeployModeStandAlone = "standalone"
|
||||
DeployModeIntegration = "integration"
|
||||
|
||||
ExtEndpoint = "ext_endpoint"
|
||||
AUTHMode = "auth_mode"
|
||||
DatabaseType = "database_type"
|
||||
|
@ -1693,3 +1693,40 @@ func TestUpdateScanJobStatus(t *testing.T) {
|
||||
err = ClearTable(models.ScanJobTable)
|
||||
assert.Nil(err)
|
||||
}
|
||||
|
||||
func TestImgScanOverview(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := ClearTable(models.ScanOverviewTable)
|
||||
assert.Nil(err)
|
||||
digest := "sha256:0204dc6e09fa57ab99ac40e415eb637d62c8b2571ecbbc9ca0eb5e2ad2b5c56f"
|
||||
res, err := GetImgScanOverview(digest)
|
||||
assert.Nil(err)
|
||||
assert.Nil(res)
|
||||
err = SetScanJobForImg(digest, 33)
|
||||
assert.Nil(err)
|
||||
res, err = GetImgScanOverview(digest)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int64(33), res.JobID)
|
||||
err = SetScanJobForImg(digest, 22)
|
||||
assert.Nil(err)
|
||||
res, err = GetImgScanOverview(digest)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int64(22), res.JobID)
|
||||
pk := "22-sha256:sdfsdfarfwefwr23r43t34ggregergerger"
|
||||
comp := &models.ComponentsOverview{
|
||||
Total: 2,
|
||||
Summary: []*models.ComponentsOverviewEntry{
|
||||
&models.ComponentsOverviewEntry{
|
||||
Sev: int(models.SevMedium),
|
||||
Count: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
err = UpdateImgScanOverview(digest, pk, models.SevMedium, comp)
|
||||
assert.Nil(err)
|
||||
res, err = GetImgScanOverview(digest)
|
||||
assert.Nil(err)
|
||||
assert.Equal(pk, res.DetailsKey)
|
||||
assert.Equal(int(models.SevMedium), res.Sev)
|
||||
assert.Equal(2, res.CompOverview.Summary[0].Count)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
@ -79,3 +80,69 @@ func scanJobQs(limit ...int) orm.QuerySeter {
|
||||
}
|
||||
return o.QueryTable(models.ScanJobTable).Limit(l)
|
||||
}
|
||||
|
||||
// SetScanJobForImg updates the scan_job_id based on the digest of image, if there's no data, it created one record.
|
||||
func SetScanJobForImg(digest string, jobID int64) error {
|
||||
o := GetOrmer()
|
||||
rec := &models.ImgScanOverview{
|
||||
Digest: digest,
|
||||
JobID: jobID,
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
created, _, err := o.ReadOrCreate(rec, "Digest")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
rec.JobID = jobID
|
||||
n, err := o.Update(rec, "JobID", "UpdateTime")
|
||||
if n == 0 {
|
||||
return fmt.Errorf("Failed to set scan job for image with digest: %s, error: %v", digest, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetImgScanOverview returns the ImgScanOverview based on the digest.
|
||||
func GetImgScanOverview(digest string) (*models.ImgScanOverview, error) {
|
||||
o := GetOrmer()
|
||||
rec := &models.ImgScanOverview{
|
||||
Digest: digest,
|
||||
}
|
||||
err := o.Read(rec)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if len(rec.CompOverviewStr) > 0 {
|
||||
co := &models.ComponentsOverview{}
|
||||
if err := json.Unmarshal([]byte(rec.CompOverviewStr), co); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rec.CompOverview = co
|
||||
}
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// UpdateImgScanOverview updates the serverity and components status of a record in img_scan_overview
|
||||
func UpdateImgScanOverview(digest, detailsKey string, sev models.Severity, compOverview *models.ComponentsOverview) error {
|
||||
o := GetOrmer()
|
||||
b, err := json.Marshal(compOverview)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rec := &models.ImgScanOverview{
|
||||
Digest: digest,
|
||||
Sev: int(sev),
|
||||
CompOverviewStr: string(b),
|
||||
DetailsKey: detailsKey,
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
n, err := o.Update(rec, "Sev", "CompOverviewStr", "DetailsKey", "UpdateTime")
|
||||
if n == 0 || err != nil {
|
||||
return fmt.Errorf("Failed to update scan overview record with digest: %s, error: %v", digest, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -27,5 +27,6 @@ func init() {
|
||||
new(Role),
|
||||
new(AccessLog),
|
||||
new(ScanJob),
|
||||
new(RepoRecord))
|
||||
new(RepoRecord),
|
||||
new(ImgScanOverview))
|
||||
}
|
||||
|
@ -31,11 +31,14 @@ type Project struct {
|
||||
OwnerName string `orm:"-" json:"owner_name"`
|
||||
Public int `orm:"column(public)" json:"public"`
|
||||
//This field does not have correspondent column in DB, this is just for UI to disable button
|
||||
Togglable bool `orm:"-"`
|
||||
|
||||
UpdateTime time.Time `orm:"update_time" json:"update_time"`
|
||||
Role int `orm:"-" json:"current_user_role_id"`
|
||||
RepoCount int `orm:"-" json:"repo_count"`
|
||||
Togglable bool `orm:"-"`
|
||||
UpdateTime time.Time `orm:"update_time" json:"update_time"`
|
||||
Role int `orm:"-" json:"current_user_role_id"`
|
||||
RepoCount int `orm:"-" json:"repo_count"`
|
||||
EnableContentTrust bool `orm:"-" json:"enable_content_trust"`
|
||||
PreventVulnerableImagesFromRunning bool `orm:"-" json:"prevent_vulnerable_images_from_running"`
|
||||
PreventVulnerableImagesFromRunningSeverity string `orm:"-" json:"prevent_vulnerable_images_from_running_severity"`
|
||||
AutomaticallyScanImagesOnPush bool `orm:"-" json:"automatically_scan_images_on_push"`
|
||||
}
|
||||
|
||||
// ProjectSorter holds an array of projects
|
||||
|
@ -19,6 +19,9 @@ import "time"
|
||||
//ScanJobTable is the name of the table whose data is mapped by ScanJob struct.
|
||||
const ScanJobTable = "img_scan_job"
|
||||
|
||||
//ScanOverviewTable is the name of the table whose data is mapped by ImgScanOverview struct.
|
||||
const ScanOverviewTable = "img_scan_overview"
|
||||
|
||||
//ScanJob is the model to represent a job for image scan in DB.
|
||||
type ScanJob struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||
@ -30,7 +33,50 @@ type ScanJob struct {
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// Severity represents the severity of a image/component in terms of vulnerability.
|
||||
type Severity int64
|
||||
|
||||
// Sevxxx is the list of severity of image after scanning.
|
||||
const (
|
||||
_ Severity = iota
|
||||
SevNone
|
||||
SevUnknown
|
||||
SevLow
|
||||
SevMedium
|
||||
SevHigh
|
||||
)
|
||||
|
||||
//TableName is required by by beego orm to map ScanJob to table img_scan_job
|
||||
func (s *ScanJob) TableName() string {
|
||||
return ScanJobTable
|
||||
}
|
||||
|
||||
//ImgScanOverview mapped to a record of image scan overview.
|
||||
type ImgScanOverview struct {
|
||||
Digest string `orm:"pk;column(image_digest)" json:"image_digest"`
|
||||
Status string `orm:"-" json:"scan_status"`
|
||||
JobID int64 `orm:"column(scan_job_id)" json:"job_id"`
|
||||
Sev int `orm:"column(severity)" json:"severity"`
|
||||
CompOverviewStr string `orm:"column(components_overview)" json:"-"`
|
||||
CompOverview *ComponentsOverview `orm:"-" json:"components"`
|
||||
DetailsKey string `orm:"column(details_key)" json:"details_key"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
//TableName ...
|
||||
func (iso *ImgScanOverview) TableName() string {
|
||||
return ScanOverviewTable
|
||||
}
|
||||
|
||||
//ComponentsOverview has the total number and a list of components number of different serverity level.
|
||||
type ComponentsOverview struct {
|
||||
Total int `json:"total"`
|
||||
Summary []*ComponentsOverviewEntry `json:"summary"`
|
||||
}
|
||||
|
||||
//ComponentsOverviewEntry ...
|
||||
type ComponentsOverviewEntry struct {
|
||||
Sev int `json:"severity"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
22
src/common/models/token.go
Normal file
22
src/common/models/token.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package models
|
||||
|
||||
// Token represents the json returned by registry token service
|
||||
type Token struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IssuedAt string `json:"issued_at"`
|
||||
}
|
108
src/common/utils/clair/client.go
Normal file
108
src/common/utils/clair/client.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clair
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
// "path"
|
||||
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// Client communicates with clair endpoint to scan image and get detailed scan result
|
||||
type Client struct {
|
||||
endpoint string
|
||||
//need to customize the logger to write output to job log.
|
||||
logger *log.Logger
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new instance of client, set the logger as the job's logger if it's used in a job handler.
|
||||
func NewClient(endpoint string, logger *log.Logger) *Client {
|
||||
if logger == nil {
|
||||
logger = log.DefaultLogger()
|
||||
}
|
||||
return &Client{
|
||||
endpoint: endpoint,
|
||||
logger: logger,
|
||||
client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// ScanLayer calls Clair's API to scan a layer.
|
||||
func (c *Client) ScanLayer(l models.ClairLayer) error {
|
||||
layer := models.ClairLayerEnvelope{
|
||||
Layer: &l,
|
||||
Error: nil,
|
||||
}
|
||||
data, err := json.Marshal(layer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logger.Infof("endpoint: %s", c.endpoint)
|
||||
c.logger.Infof("body: %s", string(data))
|
||||
req, err := http.NewRequest("POST", c.endpoint+"/v1/layers", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set(http.CanonicalHeaderKey("Content-Type"), "application/json")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
c.logger.Infof("response code: %d", resp.StatusCode)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
c.logger.Warningf("Unexpected status code: %d", resp.StatusCode)
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Unexpected status code: %d, text: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
c.logger.Infof("Returning.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetResult calls Clair's API to get layers with detailed vulnerability list
|
||||
func (c *Client) GetResult(layerName string) (*models.ClairLayerEnvelope, error) {
|
||||
req, err := http.NewRequest("GET", c.endpoint+"/v1/layers/"+layerName+"?features&vulnerabilities", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Unexpected status code: %d, text: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
var res models.ClairLayerEnvelope
|
||||
err = json.Unmarshal(b, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
37
src/common/utils/clair/utils.go
Normal file
37
src/common/utils/clair/utils.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clair
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseClairSev parse the severity of clair to Harbor's Severity type if the string is not recognized the value will be set to unknown.
|
||||
func ParseClairSev(clairSev string) models.Severity {
|
||||
sev := strings.ToLower(clairSev)
|
||||
switch sev {
|
||||
case "negligible":
|
||||
return models.SevNone
|
||||
case "low":
|
||||
return models.SevLow
|
||||
case "medium":
|
||||
return models.SevMedium
|
||||
case "high":
|
||||
return models.SevHigh
|
||||
default:
|
||||
return models.SevUnknown
|
||||
}
|
||||
}
|
35
src/common/utils/clair/utils_test.go
Normal file
35
src/common/utils/clair/utils_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package clair
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
)
|
||||
|
||||
func TestParseServerity(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
in := map[string]models.Severity{
|
||||
"negligible": models.SevNone,
|
||||
"whatever": models.SevUnknown,
|
||||
"LOW": models.SevLow,
|
||||
"Medium": models.SevMedium,
|
||||
"high": models.SevHigh,
|
||||
}
|
||||
for k, v := range in {
|
||||
assert.Equal(v, ParseClairSev(k))
|
||||
}
|
||||
}
|
@ -64,6 +64,11 @@ func New(out io.Writer, fmtter Formatter, lvl Level) *Logger {
|
||||
}
|
||||
}
|
||||
|
||||
//DefaultLogger returns the default logger within the pkg, i.e. the one used in log.Infof....
|
||||
func DefaultLogger() *Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
//SetOutput sets the output of Logger l
|
||||
func (l *Logger) SetOutput(out io.Writer) {
|
||||
l.mu.Lock()
|
||||
|
@ -25,9 +25,10 @@ import (
|
||||
"time"
|
||||
|
||||
//"github.com/vmware/harbor/src/common/config"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
token_util "github.com/vmware/harbor/src/ui/service/token"
|
||||
)
|
||||
|
||||
@ -205,11 +206,7 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []
|
||||
return
|
||||
}
|
||||
|
||||
tk := struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IssuedAt string `json:"issued_at"`
|
||||
}{}
|
||||
tk := models.Token{}
|
||||
if err = json.Unmarshal(b, &tk); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
// "time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
)
|
||||
|
||||
// Registry holds information of a registry entity
|
||||
|
@ -30,7 +30,7 @@ import (
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
)
|
||||
|
||||
// Repository holds information of a repository entity
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/common/utils/test"
|
||||
)
|
||||
|
||||
|
@ -167,3 +167,8 @@ func ExtEndpoint() (string, error) {
|
||||
func InternalTokenServiceEndpoint() string {
|
||||
return "http://ui/service/token"
|
||||
}
|
||||
|
||||
// ClairEndpoint returns the end point of clair instance, by default it's the one deployed within Harbor.
|
||||
func ClairEndpoint() string {
|
||||
return "http://clair:6060"
|
||||
}
|
||||
|
@ -217,5 +217,11 @@ func prepareScanJobData() error {
|
||||
}
|
||||
|
||||
func clearScanJobData() error {
|
||||
return dao.ClearTable(models.ScanJobTable)
|
||||
if err := dao.ClearTable(models.ScanJobTable); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dao.ClearTable(models.ScanOverviewTable); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -220,6 +220,10 @@ func (sj *ScanJob) Init() error {
|
||||
Tag: job.Tag,
|
||||
Digest: job.Digest,
|
||||
}
|
||||
err = dao.SetScanJobForImg(job.Digest, sj.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -258,11 +258,14 @@ func addImgScanTransition(sm *SM, parm *ScanJobParm) {
|
||||
Repository: parm.Repository,
|
||||
Tag: parm.Tag,
|
||||
Digest: parm.Digest,
|
||||
JobID: sm.CurrentJob.ID(),
|
||||
Logger: sm.Logger,
|
||||
}
|
||||
|
||||
layerScanHandler := &scan.LayerScanHandler{Context: ctx}
|
||||
sm.AddTransition(models.JobRunning, scan.StateInitialize, &scan.Initializer{Context: ctx})
|
||||
sm.AddTransition(scan.StateInitialize, scan.StateScanLayer, &scan.LayerScanHandler{Context: ctx})
|
||||
sm.AddTransition(scan.StateInitialize, scan.StateScanLayer, layerScanHandler)
|
||||
sm.AddTransition(scan.StateScanLayer, scan.StateScanLayer, layerScanHandler)
|
||||
sm.AddTransition(scan.StateScanLayer, scan.StateSummarize, &scan.SummarizeHandler{Context: ctx})
|
||||
sm.AddTransition(scan.StateSummarize, models.JobFinished, &StatusUpdater{sm.CurrentJob, models.JobFinished})
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/clair"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
@ -29,16 +31,15 @@ const (
|
||||
|
||||
//JobContext is for sharing data across handlers in a execution of a scan job.
|
||||
type JobContext struct {
|
||||
JobID int64
|
||||
Repository string
|
||||
Tag string
|
||||
Digest string
|
||||
//the digests of layers
|
||||
layers []string
|
||||
//each layer name has to be unique, so it should be ${img-digest}-${layer-digest}
|
||||
layerNames []string
|
||||
//the index of current layer
|
||||
//The array of data object to set as request body for layer scan.
|
||||
layers []models.ClairLayer
|
||||
current int
|
||||
//token for accessing the registry
|
||||
token string
|
||||
Logger *log.Logger
|
||||
token string
|
||||
clairClient *clair.Client
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
@ -15,7 +15,17 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/clair"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
"github.com/vmware/harbor/src/jobservice/config"
|
||||
"github.com/vmware/harbor/src/jobservice/utils"
|
||||
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Initializer will handle the initialise state pull the manifest, prepare token.
|
||||
@ -27,9 +37,60 @@ type Initializer struct {
|
||||
func (iz *Initializer) Enter() (string, error) {
|
||||
logger := iz.Context.Logger
|
||||
logger.Infof("Entered scan initializer")
|
||||
regURL, err := config.LocalRegURL()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to read regURL, error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
|
||||
repoClient, err := utils.NewRepositoryClient(regURL, false, auth.NewCookieCredential(c),
|
||||
config.InternalTokenServiceEndpoint(), iz.Context.Repository, "pull")
|
||||
if err != nil {
|
||||
logger.Errorf("An error occurred while creating repository client: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, _, payload, err := repoClient.PullManifest(iz.Context.Digest, []string{schema2.MediaTypeManifest})
|
||||
if err != nil {
|
||||
logger.Errorf("Error pulling manifest for image %s:%s :%v", iz.Context.Repository, iz.Context.Tag, err)
|
||||
return "", err
|
||||
}
|
||||
manifest, _, err := distribution.UnmarshalManifest(schema2.MediaTypeManifest, payload)
|
||||
if err != nil {
|
||||
logger.Error("Failed to unMarshal manifest from response")
|
||||
return "", err
|
||||
}
|
||||
|
||||
tk, err := utils.GetTokenForRepo(iz.Context.Repository)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
iz.Context.token = tk
|
||||
iz.Context.clairClient = clair.NewClient(config.ClairEndpoint(), logger)
|
||||
iz.prepareLayers(regURL, manifest.References())
|
||||
return StateScanLayer, nil
|
||||
}
|
||||
|
||||
func (iz *Initializer) prepareLayers(registryEndpoint string, descriptors []distribution.Descriptor) {
|
||||
// logger := iz.Context.Logger
|
||||
tokenHeader := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", iz.Context.token)}
|
||||
for _, d := range descriptors {
|
||||
if d.MediaType == schema2.MediaTypeConfig {
|
||||
continue
|
||||
}
|
||||
l := models.ClairLayer{
|
||||
Name: fmt.Sprintf("%d-%s", iz.Context.JobID, d.Digest),
|
||||
Headers: tokenHeader,
|
||||
Format: "Docker",
|
||||
Path: utils.BuildBlobURL(registryEndpoint, iz.Context.Repository, string(d.Digest)),
|
||||
}
|
||||
if len(iz.Context.layers) > 0 {
|
||||
l.ParentName = iz.Context.layers[len(iz.Context.layers)-1].Name
|
||||
}
|
||||
iz.Context.layers = append(iz.Context.layers, l)
|
||||
}
|
||||
}
|
||||
|
||||
// Exit ...
|
||||
func (iz *Initializer) Exit() error {
|
||||
return nil
|
||||
@ -43,8 +104,19 @@ type LayerScanHandler struct {
|
||||
// Enter ...
|
||||
func (ls *LayerScanHandler) Enter() (string, error) {
|
||||
logger := ls.Context.Logger
|
||||
logger.Infof("Entered scan layer handler")
|
||||
return StateSummarize, nil
|
||||
currentLayer := ls.Context.layers[ls.Context.current]
|
||||
logger.Infof("Entered scan layer handler, current: %d, layer name: %s", ls.Context.current, currentLayer.Name)
|
||||
err := ls.Context.clairClient.ScanLayer(currentLayer)
|
||||
if err != nil {
|
||||
logger.Errorf("Unexpected error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
ls.Context.current++
|
||||
if ls.Context.current == len(ls.Context.layers) {
|
||||
return StateSummarize, nil
|
||||
}
|
||||
logger.Infof("After scanning, return with next state: %s", StateScanLayer)
|
||||
return StateScanLayer, nil
|
||||
}
|
||||
|
||||
// Exit ...
|
||||
@ -61,6 +133,46 @@ type SummarizeHandler struct {
|
||||
func (sh *SummarizeHandler) Enter() (string, error) {
|
||||
logger := sh.Context.Logger
|
||||
logger.Infof("Entered summarize handler")
|
||||
layerName := sh.Context.layers[len(sh.Context.layers)-1].Name
|
||||
logger.Infof("Top layer's name: %s, will use it to get the vulnerability result of image", layerName)
|
||||
res, err := sh.Context.clairClient.GetResult(layerName)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get result from Clair, error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
vulnMap := make(map[models.Severity]int)
|
||||
features := res.Layer.Features
|
||||
totalComponents := len(features)
|
||||
logger.Infof("total features: %d", totalComponents)
|
||||
var temp models.Severity
|
||||
for _, f := range features {
|
||||
sev := models.SevNone
|
||||
for _, v := range f.Vulnerabilities {
|
||||
temp = clair.ParseClairSev(v.Severity)
|
||||
if temp > sev {
|
||||
sev = temp
|
||||
}
|
||||
}
|
||||
logger.Infof("Feature: %s, Severity: %d", f.Name, sev)
|
||||
vulnMap[sev]++
|
||||
}
|
||||
overallSev := models.SevNone
|
||||
compSummary := []*models.ComponentsOverviewEntry{}
|
||||
for k, v := range vulnMap {
|
||||
if k > overallSev {
|
||||
overallSev = k
|
||||
}
|
||||
entry := &models.ComponentsOverviewEntry{
|
||||
Sev: int(k),
|
||||
Count: v,
|
||||
}
|
||||
compSummary = append(compSummary, entry)
|
||||
}
|
||||
compOverview := &models.ComponentsOverview{
|
||||
Total: totalComponents,
|
||||
Summary: compSummary,
|
||||
}
|
||||
err = dao.UpdateImgScanOverview(sh.Context.Digest, layerName, overallSev, compOverview)
|
||||
return models.JobFinished, nil
|
||||
}
|
||||
|
||||
|
@ -15,9 +15,16 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
"net/http"
|
||||
"github.com/vmware/harbor/src/jobservice/config"
|
||||
)
|
||||
|
||||
//NewRepositoryClient create a repository client with scope type "reopsitory" and scope as the repository it would access.
|
||||
@ -51,3 +58,44 @@ func (u *userAgentModifier) Modify(req *http.Request) error {
|
||||
req.Header.Set(http.CanonicalHeaderKey("User-Agent"), u.userAgent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildBlobURL ...
|
||||
func BuildBlobURL(endpoint, repository, digest string) string {
|
||||
return fmt.Sprintf("%s/v2/%s/blobs/%s", endpoint, repository, digest)
|
||||
}
|
||||
|
||||
//GetTokenForRepo is a temp solution for job handler to get a token for clair.
|
||||
func GetTokenForRepo(repository string) (string, error) {
|
||||
u, err := url.Parse(config.InternalTokenServiceEndpoint())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Add("service", "harbor-registry")
|
||||
q.Add("scope", fmt.Sprintf("repository:%s:pull", repository))
|
||||
u.RawQuery = q.Encode()
|
||||
r, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
|
||||
r.AddCookie(c)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("Unexpected response from token service, code: %d, %s", resp.StatusCode, string(b))
|
||||
}
|
||||
tk := models.Token{}
|
||||
if err := json.Unmarshal(b, &tk); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tk.Token, nil
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import (
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/notary"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
|
@ -27,7 +27,7 @@ import (
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
|
@ -29,7 +29,7 @@ import (
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/projectmanager"
|
||||
)
|
||||
@ -388,6 +388,29 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
|
||||
endpoint, err := config.RegistryURL()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get registry URL: %v", err)
|
||||
continue
|
||||
}
|
||||
client, err := NewRepositoryClient(endpoint, true,
|
||||
"admin", repoInR, "repository", repoInR, "pull")
|
||||
if err != nil {
|
||||
log.Errorf("failed to create repository client: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
exist, err = repositoryExist(repoInR, client)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check the existence of repository %s: %v", repoInR, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
|
||||
needsAdd = append(needsAdd, repoInR)
|
||||
}
|
||||
|
||||
|
@ -95,8 +95,7 @@ func initSecretStore() {
|
||||
}
|
||||
|
||||
func initProjectManager() {
|
||||
if len(DeployMode()) == 0 ||
|
||||
DeployMode() == common.DeployModeStandAlone {
|
||||
if !WithAdmiral() {
|
||||
log.Info("initializing the project manager based on database...")
|
||||
GlobalProjectMgr = &db.ProjectManager{}
|
||||
}
|
||||
@ -332,9 +331,3 @@ func AdmiralEndpoint() string {
|
||||
func WithAdmiral() bool {
|
||||
return len(AdmiralEndpoint()) > 0
|
||||
}
|
||||
|
||||
// DeployMode returns the deploy mode
|
||||
// TODO read from adminserver
|
||||
func DeployMode() string {
|
||||
return common.DeployModeStandAlone
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
"strings"
|
||||
|
||||
beegoctx "github.com/astaxie/beego/context"
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/security"
|
||||
"github.com/vmware/harbor/src/common/security/rbac"
|
||||
@ -30,6 +29,7 @@ import (
|
||||
"github.com/vmware/harbor/src/ui/auth"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/projectmanager"
|
||||
"github.com/vmware/harbor/src/ui/projectmanager/pms"
|
||||
)
|
||||
|
||||
type key string
|
||||
@ -133,15 +133,14 @@ func fillContext(ctx *beegoctx.Context) {
|
||||
}
|
||||
|
||||
func getProjectManager(ctx *beegoctx.Context) projectmanager.ProjectManager {
|
||||
if len(config.DeployMode()) == 0 ||
|
||||
config.DeployMode() == common.DeployModeStandAlone {
|
||||
if !config.WithAdmiral() {
|
||||
log.Info("filling a project manager based on database...")
|
||||
return config.GlobalProjectMgr
|
||||
}
|
||||
|
||||
// TODO create project manager based on pms
|
||||
log.Info("filling a project manager based on pms...")
|
||||
return nil
|
||||
log.Info("filling a project manager based on PMS...")
|
||||
// TODO pass the token to the function
|
||||
return pms.NewProjectManager(config.AdmiralEndpoint(), "")
|
||||
}
|
||||
|
||||
// GetSecurityContext tries to get security context from request and returns it
|
||||
|
422
src/ui/projectmanager/pms/pm.go
Normal file
422
src/ui/projectmanager/pms/pm.go
Normal file
@ -0,0 +1,422 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
er "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
var transport = &http.Transport{}
|
||||
|
||||
// ProjectManager implements projectmanager.ProjecdtManager interface
|
||||
// base on project management service
|
||||
type ProjectManager struct {
|
||||
endpoint string
|
||||
token string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type user struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type project struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Public bool `json:"isPublic"`
|
||||
OwnerID string `json:"documentOwner"`
|
||||
CustomProperties map[string]string `json:"customProperties"`
|
||||
Administrators []*user `json:"administrators"`
|
||||
Developers []*user `json:"members"`
|
||||
Guests []*user `json:"guests"` // TODO the json name needs to be modified according to the API
|
||||
}
|
||||
|
||||
// NewProjectManager returns an instance of ProjectManager
|
||||
func NewProjectManager(endpoint, token string) *ProjectManager {
|
||||
return &ProjectManager{
|
||||
endpoint: strings.TrimRight(endpoint, "/"),
|
||||
token: token,
|
||||
client: &http.Client{
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (p *ProjectManager) Get(projectIDOrName interface{}) (*models.Project, error) {
|
||||
project, err := p.get(projectIDOrName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convert(project)
|
||||
}
|
||||
|
||||
func (p *ProjectManager) get(projectIDOrName interface{}) (*project, error) {
|
||||
m := map[string]string{}
|
||||
if id, ok := projectIDOrName.(int64); ok {
|
||||
m["customProperties.__harborId"] = strconv.FormatInt(id, 10)
|
||||
} else if name, ok := projectIDOrName.(string); ok {
|
||||
m["name"] = name
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported type: %v", projectIDOrName)
|
||||
}
|
||||
|
||||
projects, err := p.filter(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(projects) != 1 {
|
||||
return nil, fmt.Errorf("unexpected size of project list: %d != 1", len(projects))
|
||||
}
|
||||
|
||||
return projects[0], nil
|
||||
}
|
||||
|
||||
func (p *ProjectManager) filter(m map[string]string) ([]*project, error) {
|
||||
query := ""
|
||||
for k, v := range m {
|
||||
if len(query) == 0 {
|
||||
query += "?"
|
||||
} else {
|
||||
query += "&"
|
||||
}
|
||||
query += fmt.Sprintf("$filter=%s eq '%s'", k, v)
|
||||
}
|
||||
|
||||
path := "/projects" + query
|
||||
data, err := p.send(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parse(data)
|
||||
}
|
||||
|
||||
// parse the response of GET /projects?xxx to project list
|
||||
func parse(b []byte) ([]*project, error) {
|
||||
documents := &struct {
|
||||
//TotalCount int64 `json:"totalCount"`
|
||||
//DocumentCount int64 `json:"documentCount"`
|
||||
Projects map[string]*project `json:"documents"`
|
||||
}{}
|
||||
if err := json.Unmarshal(b, documents); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects := []*project{}
|
||||
for link, project := range documents.Projects {
|
||||
project.ID = strings.TrimLeft(link, "/projects/")
|
||||
projects = append(projects, project)
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func convert(p *project) (*models.Project, error) {
|
||||
if p == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
project := &models.Project{
|
||||
Name: p.Name,
|
||||
}
|
||||
if p.Public {
|
||||
project.Public = 1
|
||||
}
|
||||
|
||||
value := p.CustomProperties["__harborId"]
|
||||
if len(value) == 0 {
|
||||
return nil, fmt.Errorf("property __harborId is null")
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse __harborId %s to int64: %v", value, err)
|
||||
}
|
||||
project.ProjectID = id
|
||||
|
||||
value = p.CustomProperties["__enableContentTrust"]
|
||||
if len(value) != 0 {
|
||||
enable, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse __enableContentTrust %s to bool: %v", value, err)
|
||||
}
|
||||
project.EnableContentTrust = enable
|
||||
}
|
||||
|
||||
value = p.CustomProperties["__preventVulnerableImagesFromRunning"]
|
||||
if len(value) != 0 {
|
||||
prevent, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse __preventVulnerableImagesFromRunning %s to bool: %v", value, err)
|
||||
}
|
||||
project.PreventVulnerableImagesFromRunning = prevent
|
||||
}
|
||||
|
||||
value = p.CustomProperties["__preventVulnerableImagesFromRunningSeverity"]
|
||||
if len(value) != 0 {
|
||||
project.PreventVulnerableImagesFromRunningSeverity = value
|
||||
}
|
||||
|
||||
value = p.CustomProperties["__automaticallyScanImagesOnPush"]
|
||||
if len(value) != 0 {
|
||||
scan, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse __automaticallyScanImagesOnPush %s to bool: %v", value, err)
|
||||
}
|
||||
project.AutomaticallyScanImagesOnPush = scan
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
// IsPublic ...
|
||||
func (p *ProjectManager) IsPublic(projectIDOrName interface{}) (bool, error) {
|
||||
project, err := p.get(projectIDOrName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if project == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return project.Public, nil
|
||||
}
|
||||
|
||||
// Exist ...
|
||||
func (p *ProjectManager) Exist(projectIDOrName interface{}) (bool, error) {
|
||||
project, err := p.get(projectIDOrName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return project != nil, nil
|
||||
}
|
||||
|
||||
// GetRoles ...
|
||||
// TODO empty this method after implementing security context with auth context
|
||||
func (p *ProjectManager) GetRoles(username string, projectIDOrName interface{}) ([]int, error) {
|
||||
if len(username) == 0 || projectIDOrName == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
id, err := p.getIDbyHarborIDOrName(projectIDOrName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get expanded project which contains role info by GET /projects/id?expand=true
|
||||
path := fmt.Sprintf("/projects/%s?expand=true", id)
|
||||
data, err := p.send(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pro := &project{}
|
||||
if err = json.Unmarshal(data, pro); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := []int{}
|
||||
|
||||
for _, user := range pro.Administrators {
|
||||
if user.Email == username {
|
||||
roles = append(roles, common.RoleProjectAdmin)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range pro.Developers {
|
||||
if user.Email == username {
|
||||
roles = append(roles, common.RoleDeveloper)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range pro.Guests {
|
||||
if user.Email == username {
|
||||
roles = append(roles, common.RoleGuest)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (p *ProjectManager) getIDbyHarborIDOrName(projectIDOrName interface{}) (string, error) {
|
||||
pro, err := p.get(projectIDOrName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pro == nil {
|
||||
return "", fmt.Errorf("project %v not found", projectIDOrName)
|
||||
}
|
||||
|
||||
return pro.ID, nil
|
||||
}
|
||||
|
||||
// GetPublic ...
|
||||
func (p *ProjectManager) GetPublic() ([]*models.Project, error) {
|
||||
m := map[string]string{
|
||||
"isPublic": "true",
|
||||
}
|
||||
|
||||
projects, err := p.filter(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*models.Project{}
|
||||
for _, p := range projects {
|
||||
project, err := convert(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, project)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetByMember ...
|
||||
func (p *ProjectManager) GetByMember(username string) ([]*models.Project, error) {
|
||||
// TODO add implement
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create ...
|
||||
func (p *ProjectManager) Create(pro *models.Project) (int64, error) {
|
||||
proj := &project{
|
||||
CustomProperties: make(map[string]string),
|
||||
}
|
||||
proj.Name = pro.Name
|
||||
proj.Public = pro.Public == 1
|
||||
proj.CustomProperties["__enableContentTrust"] = strconv.FormatBool(pro.EnableContentTrust)
|
||||
proj.CustomProperties["__preventVulnerableImagesFromRunning"] = strconv.FormatBool(pro.PreventVulnerableImagesFromRunning)
|
||||
proj.CustomProperties["__preventVulnerableImagesFromRunningSeverity"] = pro.PreventVulnerableImagesFromRunningSeverity
|
||||
proj.CustomProperties["__automaticallyScanImagesOnPush"] = strconv.FormatBool(pro.AutomaticallyScanImagesOnPush)
|
||||
|
||||
// TODO remove the logic if Admiral generates the harborId
|
||||
proj.CustomProperties["__harborId"] = strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
|
||||
data, err := json.Marshal(proj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
b, err := p.send(http.MethodPost, "/projects", bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
proj = &project{}
|
||||
if err = json.Unmarshal(b, proj); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
pp, err := convert(proj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return pp.ProjectID, err
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (p *ProjectManager) Delete(projectIDOrName interface{}) error {
|
||||
id, err := p.getIDbyHarborIDOrName(projectIDOrName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = p.send(http.MethodDelete, fmt.Sprintf("/projects/%s", id), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update ...
|
||||
func (p *ProjectManager) Update(projectIDOrName interface{}, project *models.Project) error {
|
||||
return errors.New("project update is unsupported")
|
||||
}
|
||||
|
||||
// GetAll ...
|
||||
func (p *ProjectManager) GetAll(query *models.ProjectQueryParam) ([]*models.Project, error) {
|
||||
return nil, errors.New("get all projects is unsupported")
|
||||
}
|
||||
|
||||
// GetTotal ...
|
||||
func (p *ProjectManager) GetTotal(query *models.ProjectQueryParam) (int64, error) {
|
||||
return 0, errors.New("get total of projects is unsupported")
|
||||
}
|
||||
|
||||
// GetHasReadPerm returns all projects that user has read perm to
|
||||
// TODO maybe can be removed as search isn't implemented in integration mode
|
||||
func (p *ProjectManager) GetHasReadPerm(username ...string) ([]*models.Project, error) {
|
||||
// TODO add implement
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, error) {
|
||||
req, err := http.NewRequest(method, p.endpoint+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("x-xenon-auth-token", p.token)
|
||||
|
||||
url := req.URL.String()
|
||||
|
||||
req.URL.RawQuery = req.URL.Query().Encode()
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
log.Debugf("\"%s %s\" failed", req.Method, url)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
log.Debugf("\"%s %s\" %d", req.Method, url, resp.StatusCode)
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, &er.Error{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
452
src/ui/projectmanager/pms/pm_test.go
Normal file
452
src/ui/projectmanager/pms/pm_test.go
Normal file
@ -0,0 +1,452 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pms
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
)
|
||||
|
||||
var (
|
||||
endpoint = "http://127.0.0.1:8282"
|
||||
token = ""
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
//nil project
|
||||
pro, err := convert(nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//project without property __harborId
|
||||
p := &project{}
|
||||
pro, err = convert(p)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//project with invalid __harborId
|
||||
p = &project{
|
||||
CustomProperties: map[string]string{
|
||||
"__harborId": "invalid_value",
|
||||
},
|
||||
}
|
||||
pro, err = convert(p)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//project with invalid __enableContentTrust
|
||||
p = &project{
|
||||
CustomProperties: map[string]string{
|
||||
"__enableContentTrust": "invalid_value",
|
||||
},
|
||||
}
|
||||
pro, err = convert(p)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//project with invalid __preventVulnerableImagesFromRunning
|
||||
p = &project{
|
||||
CustomProperties: map[string]string{
|
||||
"__preventVulnerableImagesFromRunning": "invalid_value",
|
||||
},
|
||||
}
|
||||
pro, err = convert(p)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//project with invalid __automaticallyScanImagesOnPush
|
||||
p = &project{
|
||||
CustomProperties: map[string]string{
|
||||
"__automaticallyScanImagesOnPush": "invalid_value",
|
||||
},
|
||||
}
|
||||
pro, err = convert(p)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, pro)
|
||||
|
||||
//valid project
|
||||
p = &project{
|
||||
Name: "test",
|
||||
Public: true,
|
||||
CustomProperties: map[string]string{
|
||||
"__harborId": "1",
|
||||
"__enableContentTrust": "true",
|
||||
"__preventVulnerableImagesFromRunning": "true",
|
||||
"__preventVulnerableImagesFromRunningSeverity": "medium",
|
||||
"__automaticallyScanImagesOnPush": "true",
|
||||
},
|
||||
}
|
||||
pro, err = convert(p)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, pro)
|
||||
assert.Equal(t, "test", pro.Name)
|
||||
assert.Equal(t, 1, pro.Public)
|
||||
assert.Equal(t, int64(1), pro.ProjectID)
|
||||
assert.True(t, pro.EnableContentTrust)
|
||||
assert.True(t, pro.PreventVulnerableImagesFromRunning)
|
||||
assert.Equal(t, "medium", pro.PreventVulnerableImagesFromRunningSeverity)
|
||||
assert.True(t, pro.AutomaticallyScanImagesOnPush)
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
data := `{
|
||||
"totalCount": 2,
|
||||
"documentLinks": [
|
||||
"/projects/default-project",
|
||||
"/projects/fc6c6c7ddd430875551449a65e7c8"
|
||||
],
|
||||
"documents": {
|
||||
"/projects/fc6c6c7ddd430875551449a65e7c8": {
|
||||
"isPublic": false,
|
||||
"description": "This is a test project.",
|
||||
"id": "41427587-70e9-4671-9a9e-b9def0a07bb7",
|
||||
"name": "project02",
|
||||
"customProperties": {
|
||||
"__harborId": "2",
|
||||
"__enableContentTrust": "true",
|
||||
"__preventVulnerableImagesFromRunning": "true",
|
||||
"__preventVulnerableImagesFromRunningSeverity": "medium",
|
||||
"__automaticallyScanImagesOnPush": "false"
|
||||
},
|
||||
"documentVersion": 0,
|
||||
"documentEpoch": 0,
|
||||
"documentKind": "com:vmware:admiral:auth:project:ProjectService:ProjectState",
|
||||
"documentSelfLink": "/projects/fc6c6c7ddd430875551449a65e7c8",
|
||||
"documentUpdateTimeMicros": 1496729973549001,
|
||||
"documentUpdateAction": "POST",
|
||||
"documentExpirationTimeMicros": 0,
|
||||
"documentOwner": "f65900c4-2b6a-4671-8cf7-c17340dd3d39"
|
||||
},
|
||||
"/projects/default-project": {
|
||||
"isPublic": false,
|
||||
"administratorsUserGroupLink": "/core/authz/user-groups/fc6c6c7ddd43087555143835bcaf8",
|
||||
"membersUserGroupLink": "/core/authz/user-groups/fc6c6c7ddd43087555143835bde80",
|
||||
"id": "default-project",
|
||||
"name": "default-project",
|
||||
"customProperties": {
|
||||
"__harborId": "2",
|
||||
"__enableContentTrust": "true",
|
||||
"__preventVulnerableImagesFromRunning": "true",
|
||||
"__preventVulnerableImagesFromRunningSeverity": "medium",
|
||||
"__automaticallyScanImagesOnPush": "false"
|
||||
},
|
||||
"documentVersion": 0,
|
||||
"documentEpoch": 0,
|
||||
"documentKind": "com:vmware:admiral:auth:project:ProjectService:ProjectState",
|
||||
"documentSelfLink": "/projects/default-project",
|
||||
"documentUpdateTimeMicros": 1496725292012001,
|
||||
"documentUpdateAction": "POST",
|
||||
"documentExpirationTimeMicros": 0,
|
||||
"documentOwner": "f65900c4-2b6a-4671-8cf7-c17340dd3d39",
|
||||
"documentAuthPrincipalLink": "/core/authz/system-user"
|
||||
}
|
||||
},
|
||||
"documentCount": 2,
|
||||
"queryTimeMicros": 1,
|
||||
"documentVersion": 0,
|
||||
"documentUpdateTimeMicros": 0,
|
||||
"documentExpirationTimeMicros": 0,
|
||||
"documentOwner": "f65900c4-2b6a-4671-8cf7-c17340dd3d39"
|
||||
}`
|
||||
|
||||
projects, err := parse([]byte(data))
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(projects))
|
||||
|
||||
ids := []string{projects[0].ID, projects[1].ID}
|
||||
sort.Strings(ids)
|
||||
|
||||
assert.Equal(t, "default-project", ids[0])
|
||||
assert.Equal(t, "fc6c6c7ddd430875551449a65e7c8", ids[1])
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
name := "project_for_pm_based_on_pms"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
// get by invalid input type
|
||||
_, err = pm.Get([]string{})
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// get by invalid ID
|
||||
project, err := pm.Get(int64(0))
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, project)
|
||||
|
||||
// get by invalid name
|
||||
project, err = pm.Get("invalid_name")
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, project)
|
||||
|
||||
// get by valid ID
|
||||
project, err = pm.Get(id)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, id, project.ProjectID)
|
||||
|
||||
// get by valid name
|
||||
project, err = pm.Get(name)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, id, project.ProjectID)
|
||||
}
|
||||
|
||||
func TestIsPublic(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
|
||||
// invalid input type
|
||||
public, err := pm.IsPublic([]string{})
|
||||
assert.NotNil(t, err)
|
||||
assert.False(t, public)
|
||||
|
||||
// non-exist project
|
||||
public, err = pm.IsPublic(int64(0))
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, public)
|
||||
|
||||
// public project
|
||||
name := "project_for_pm_based_on_pms_public"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
public, err = pm.IsPublic(id)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, public)
|
||||
|
||||
public, err = pm.IsPublic(name)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, public)
|
||||
|
||||
// private project
|
||||
name = "project_for_pm_based_on_pms_private"
|
||||
id, err = pm.Create(&models.Project{
|
||||
Name: name,
|
||||
Public: 0,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
public, err = pm.IsPublic(id)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, public)
|
||||
|
||||
public, err = pm.IsPublic(name)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, public)
|
||||
}
|
||||
|
||||
func TestExist(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
|
||||
// invalid input type
|
||||
exist, err := pm.Exist([]string{})
|
||||
assert.NotNil(t, err)
|
||||
assert.False(t, exist)
|
||||
|
||||
// non-exist project
|
||||
exist, err = pm.Exist(int64(0))
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, exist)
|
||||
|
||||
// exist project
|
||||
name := "project_for_pm_based_on_pms"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
exist, err = pm.Exist(id)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, exist)
|
||||
|
||||
exist, err = pm.Exist(name)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, exist)
|
||||
}
|
||||
|
||||
func TestGetRoles(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
|
||||
// nil username, nil project
|
||||
roles, err := pm.GetRoles("", nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Zero(t, len(roles))
|
||||
|
||||
// non-exist project
|
||||
_, err = pm.GetRoles("user01", "non_exist_project")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// exist project
|
||||
name := "project_for_pm_based_on_pms"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
roles, err = pm.GetRoles("user01", id)
|
||||
assert.Nil(t, err)
|
||||
assert.Zero(t, len(roles))
|
||||
|
||||
// TODO add test cases for real role of user
|
||||
}
|
||||
|
||||
func TestGetPublic(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
|
||||
projects, err := pm.GetPublic()
|
||||
assert.Nil(t, nil)
|
||||
size := len(projects)
|
||||
|
||||
name := "project_for_pm_based_on_pms"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
projects, err = pm.GetPublic()
|
||||
assert.Nil(t, nil)
|
||||
assert.Equal(t, size+1, len(projects))
|
||||
|
||||
found := false
|
||||
for _, project := range projects {
|
||||
if project.ProjectID == id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
// TODO add test case
|
||||
func TestGetByMember(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
|
||||
name := "project_for_pm_based_on_pms"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
Public: 1,
|
||||
EnableContentTrust: true,
|
||||
PreventVulnerableImagesFromRunning: true,
|
||||
PreventVulnerableImagesFromRunningSeverity: "medium",
|
||||
AutomaticallyScanImagesOnPush: true,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
|
||||
project, err := pm.Get(id)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, name, project.Name)
|
||||
assert.Equal(t, 1, project.Public)
|
||||
assert.True(t, project.EnableContentTrust)
|
||||
assert.True(t, project.PreventVulnerableImagesFromRunning)
|
||||
assert.Equal(t, "medium", project.PreventVulnerableImagesFromRunningSeverity)
|
||||
assert.True(t, project.AutomaticallyScanImagesOnPush)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
|
||||
// non-exist project
|
||||
err := pm.Delete(int64(0))
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// delete by ID
|
||||
name := "project_for_pm_based_on_pms_id"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
err = pm.Delete(id)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// delete by name
|
||||
name = "project_for_pm_based_on_pms_name"
|
||||
id, err = pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
err = pm.Delete(name)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
err := pm.Update(nil, nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestGetAll(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
_, err := pm.GetAll(nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestGetTotal(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
_, err := pm.GetTotal(nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
// TODO add test case
|
||||
func TestGetHasReadPerm(t *testing.T) {
|
||||
|
||||
}
|
@ -23,12 +23,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/registry/auth/token"
|
||||
"github.com/docker/libtrust"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/security"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
|
||||
"github.com/docker/distribution/registry/auth/token"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -132,18 +132,16 @@ func MakeRawToken(username, service string, access []*token.ResourceActions) (to
|
||||
return rs, expiresIn, issuedAt, nil
|
||||
}
|
||||
|
||||
type tokenJSON struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IssuedAt string `json:"issued_at"`
|
||||
}
|
||||
|
||||
func makeToken(username, service string, access []*token.ResourceActions) (*tokenJSON, error) {
|
||||
func makeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) {
|
||||
raw, expires, issued, err := MakeRawToken(username, service, access)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tokenJSON{raw, expires, issued.Format(time.RFC3339)}, nil
|
||||
return &models.Token{
|
||||
Token: raw,
|
||||
ExpiresIn: expires,
|
||||
IssuedAt: issued.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func permToActions(p string) []string {
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/registry/auth/token"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/security"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
@ -70,7 +71,7 @@ func InitCreators() {
|
||||
|
||||
// Creator creates a token ready to be served based on the http request.
|
||||
type Creator interface {
|
||||
Create(r *http.Request) (*tokenJSON, error)
|
||||
Create(r *http.Request) (*models.Token, error)
|
||||
}
|
||||
|
||||
type imageParser interface {
|
||||
@ -178,7 +179,7 @@ func (e *unauthorizedError) Error() string {
|
||||
return "Unauthorized"
|
||||
}
|
||||
|
||||
func (g generalCreator) Create(r *http.Request) (*tokenJSON, error) {
|
||||
func (g generalCreator) Create(r *http.Request) (*models.Token, error) {
|
||||
var err error
|
||||
scopes := parseScopes(r.URL)
|
||||
log.Debugf("scopes: %v", scopes)
|
||||
|
@ -171,7 +171,7 @@ func TestBasicParser(t *testing.T) {
|
||||
for _, rec := range testList {
|
||||
r, err := p.parse(rec.input)
|
||||
if rec.expectError {
|
||||
assert.Error(t, err, "Expected error for input: %s", rec.input)
|
||||
assert.Error(t, err, fmt.Sprintf("Expected error for input: %s", rec.input))
|
||||
} else {
|
||||
assert.Nil(t, err, "Expected no error for input: %s", rec.input)
|
||||
assert.Equal(t, rec.expect, *r, "result mismatch for input: %s", rec.input)
|
||||
@ -193,7 +193,7 @@ func TestEndpointParser(t *testing.T) {
|
||||
for _, rec := range testList {
|
||||
r, err := p.parse(rec.input)
|
||||
if rec.expectError {
|
||||
assert.Error(t, err, "Expected error for input: %s", rec.input)
|
||||
assert.Error(t, err, fmt.Sprintf("Expected error for input: %s", rec.input))
|
||||
} else {
|
||||
assert.Nil(t, err, "Expected no error for input: %s", rec.input)
|
||||
assert.Equal(t, rec.expect, *r, "result mismatch for input: %s", rec.input)
|
||||
|
@ -52,10 +52,9 @@ If no parameters are passed to **'forRoot'**, the module will be initialized wit
|
||||
|
||||
* **Registry log view**
|
||||
|
||||
Use **withTitle** to set whether self-contained a header with title or not. Default is **false**, that means no header is existing.
|
||||
```
|
||||
//No @Input properties
|
||||
|
||||
<hbr-log></hbr-log>
|
||||
<hbr-log [withTitle]="..."></hbr-log>
|
||||
```
|
||||
|
||||
* **Replication Management View**
|
||||
@ -85,8 +84,18 @@ If **projectId** is set to the id of specified project, then only show the repli
|
||||
|
||||
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
|
||||
|
||||
**tagClickEvent** is an @output event emitter for you to catch the tag click events.
|
||||
|
||||
```
|
||||
<hbr-repository-stackview [projectId]="..." [hasSignedIn]="..." [hasProjectAdminRole]="..."></hbr-repository-stackview>
|
||||
<hbr-repository-stackview [projectId]="..." [hasSignedIn]="..." [hasProjectAdminRole]="..." (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
|
||||
|
||||
...
|
||||
|
||||
watchTagClickEvent(tag: Tag): void {
|
||||
//Process tag
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Configurations
|
||||
@ -96,7 +105,7 @@ All the related configurations are defined in the **HarborModuleConfig** interfa
|
||||
The base configuration for the module. Mainly used to define the relevant endpoints of services which are in charge of retrieving data from backend APIs. It's a 'OpaqueToken' and defined by 'IServiceConfig' interface. If **config** is not set, the default value will be used.
|
||||
```
|
||||
export const DefaultServiceConfig: IServiceConfig = {
|
||||
systemInfoEndpoint: "/api/system",
|
||||
systemInfoEndpoint: "/api/systeminfo",
|
||||
repositoryBaseEndpoint: "/api/repositories",
|
||||
logBaseEndpoint: "/api/logs",
|
||||
targetBaseEndpoint: "/api/targets",
|
||||
@ -126,6 +135,8 @@ HarborLibraryModule.forRoot({
|
||||
|
||||
```
|
||||
It supports partially overriding. For the items not overridden, default values will be adopted. The items contained in **config** are:
|
||||
* **systemInfoEndpoint:** The base endpoint of the service used to get the related system configurations. Default value is "/api/systeminfo".
|
||||
|
||||
* **repositoryBaseEndpoint:** The base endpoint of the service used to handle the repositories of registry and/or tags of repository. Default value is "/api/repositories".
|
||||
|
||||
* **logBaseEndpoint:** The base endpoint of the service used to handle the recent access logs. Default is "/api/logs".
|
||||
@ -578,32 +589,39 @@ HarborLibraryModule.forRoot({
|
||||
* **ScanningResultService:** Get the vulnerabilities scanning results for the specified tag.
|
||||
```
|
||||
@Injectable()
|
||||
/**
|
||||
* Get the vulnerabilities scanning results for the specified tag.
|
||||
*
|
||||
* @export
|
||||
* @abstract
|
||||
* @class ScanningResultService
|
||||
*/
|
||||
export class MyScanningResultService extends ScanningResultService {
|
||||
/**
|
||||
* Get the summary of vulnerability scanning result.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary)}
|
||||
* @returns {(Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary)}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary {
|
||||
...
|
||||
}
|
||||
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the detailed vulnerabilities scanning results.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[])}
|
||||
* @returns {(Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[])}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[] {
|
||||
...
|
||||
}
|
||||
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
@ -612,4 +630,30 @@ HarborLibraryModule.forRoot({
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
* **SystemInfoService:** Get related system configurations.
|
||||
```
|
||||
/**
|
||||
* Get System information about current backend server.
|
||||
* @abstract
|
||||
* @class
|
||||
*/
|
||||
export class MySystemInfoService extends SystemInfoService {
|
||||
/**
|
||||
* Get global system information.
|
||||
* @abstract
|
||||
* @returns
|
||||
*/
|
||||
getSystemInfo(): Observable<SystemInfo> | Promise<SystemInfo> | SystemInfo {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
systemInfoService: { provide: SystemInfoService, useClass: MySystemInfoService }
|
||||
})
|
||||
...
|
||||
|
||||
```
|
@ -1,10 +1,15 @@
|
||||
export const ENDPOINT_STYLE: string = `
|
||||
.option-left {
|
||||
padding-left: 16px;
|
||||
margin-top: 24px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 36px;
|
||||
}
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
`;
|
@ -1,17 +1,17 @@
|
||||
export const ENDPOINT_TEMPLATE: string = `
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="row flex-items-xs-between" style="height: 24px;">
|
||||
<div class="flex-items-xs-middle option-left">
|
||||
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.ENDPOINT' | translate}}</button>
|
||||
<create-edit-endpoint (reload)="reload($event)"></create-edit-endpoint>
|
||||
</div>
|
||||
<div class="flex-items-xs-middle option-right">
|
||||
<hbr-filter filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></hbr-filter>
|
||||
<a href="javascript:void(0)" (click)="refreshTargets()">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refreshTargets()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -20,6 +20,7 @@ export const ENDPOINT_TEMPLATE: string = `
|
||||
<clr-dg-column [clrDgField]="'name'">{{'DESTINATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'endpoint'">{{'DESTINATION.URL' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'DESTINATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
|
||||
@ -37,4 +38,6 @@ export const ENDPOINT_TEMPLATE: string = `
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
</div>
|
||||
`;
|
@ -31,6 +31,7 @@ export class FilterComponent implements OnInit {
|
||||
|
||||
placeHolder: string = "";
|
||||
filterTerms = new Subject<string>();
|
||||
isExpanded: boolean = false;
|
||||
|
||||
@Output("filter") private filterEvt = new EventEmitter<string>();
|
||||
|
||||
@ -39,6 +40,8 @@ export class FilterComponent implements OnInit {
|
||||
public set flPlaceholder(placeHolder: string) {
|
||||
this.placeHolder = placeHolder;
|
||||
}
|
||||
@Input() expandMode: boolean = false;
|
||||
@Input() withDivider: boolean = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.filterTerms
|
||||
@ -54,4 +57,16 @@ export class FilterComponent implements OnInit {
|
||||
//Send out filter terms
|
||||
this.filterTerms.next(this.currentValue.trim());
|
||||
}
|
||||
|
||||
onClick(): void {
|
||||
//Only enabled when expandMode is set to false
|
||||
if(this.expandMode){
|
||||
return;
|
||||
}
|
||||
this.isExpanded = !this.isExpanded;
|
||||
}
|
||||
|
||||
public get isShowSearchBox(): boolean {
|
||||
return this.expandMode || (!this.expandMode && this.isExpanded);
|
||||
}
|
||||
}
|
@ -4,8 +4,9 @@
|
||||
|
||||
export const FILTER_TEMPLATE: string = `
|
||||
<span>
|
||||
<clr-icon shape="filter" size="12" class="is-solid filter-icon"></clr-icon>
|
||||
<input type="text" style="padding-left: 15px;" (keyup)="valueChange()" placeholder="{{placeHolder}}" [(ngModel)]="currentValue"/>
|
||||
<clr-icon shape="search" size="20" class="search-btn" [class.filter-icon]="isShowSearchBox" (click)="onClick()"></clr-icon>
|
||||
<input [hidden]="!isShowSearchBox" type="text" style="padding-left: 15px;" (keyup)="valueChange()" placeholder="{{placeHolder}}" [(ngModel)]="currentValue"/>
|
||||
<span class="filter-divider" *ngIf="withDivider"></span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@ -14,4 +15,25 @@ export const FILTER_STYLES: string = `
|
||||
position: relative;
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
.filter-divider {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 2px;
|
||||
background-color: #cccccc;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
position: relative;
|
||||
top: 9px;
|
||||
margin-right: 6px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
`;
|
@ -1,6 +1,5 @@
|
||||
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<confirmation-dialog #toggleConfirmDialog (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
|
||||
<confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
|
||||
<div>
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'project_name'" *ngIf="projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
|
||||
@ -8,6 +7,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<clr-dg-column [clrDgField]="'target_name'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="enabledComparator">{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let p of changedRules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(!projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="editRule(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
|
||||
@ -37,4 +37,8 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'REPLICATION.OF' | translate}} {{pagination.totalItems }} {{'REPLICATION.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>`;
|
||||
</clr-datagrid>
|
||||
<confirmation-dialog #toggleConfirmDialog (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
|
||||
<confirmation-dialog #deletionConfirmDialog (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
|
||||
</div>
|
||||
`;
|
@ -3,6 +3,7 @@ export const LIST_REPOSITORY_TEMPLATE = `
|
||||
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'>
|
||||
<clr-dg-action-overflow [hidden]="!hasProjectAdminRole">
|
||||
<button class="action-item" (click)="deleteRepo(r.name)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
|
@ -11,7 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
AccessLogService,
|
||||
@ -23,6 +23,7 @@ import { ErrorHandler } from '../error-handler/index';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
import { LOG_TEMPLATE, LOG_STYLES } from './recent-log.template';
|
||||
import { DEFAULT_PAGE_SIZE } from '../utils';
|
||||
|
||||
import { Comparator, State } from 'clarity-angular';
|
||||
|
||||
@ -37,11 +38,12 @@ export class RecentLogComponent implements OnInit {
|
||||
logsCache: AccessLog;
|
||||
loading: boolean = true;
|
||||
currentTerm: string;
|
||||
@Input() withTitle: boolean = false;
|
||||
|
||||
pageSize: number = 15;
|
||||
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||
currentPage: number = 0;
|
||||
|
||||
opTimeComparator: Comparator<AccessLog> = new CustomComparator<AccessLog>('op_time', 'date');
|
||||
opTimeComparator: Comparator<AccessLogItem> = new CustomComparator<AccessLogItem>('op_time', 'date');
|
||||
|
||||
constructor(
|
||||
private logService: AccessLogService,
|
||||
|
@ -4,11 +4,11 @@
|
||||
|
||||
export const LOG_TEMPLATE: string = `
|
||||
<div>
|
||||
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
|
||||
<h2 class="h2-log-override" *ngIf="withTitle">{{'SIDE_NAV.LOGS' | translate}}</h2>
|
||||
<div class="row flex-items-xs-between flex-items-xs-bottom">
|
||||
<div></div>
|
||||
<div class="action-head-pos">
|
||||
<hbr-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></hbr-filter>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></hbr-filter>
|
||||
<span (click)="refresh()" class="refresh-btn">
|
||||
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
|
||||
<span class="spinner spinner-inline" [hidden]="!inProgress"></span>
|
||||
@ -47,6 +47,7 @@ export const LOG_STYLES: string = `
|
||||
|
||||
.action-head-pos {
|
||||
padding-right: 18px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
@ -54,7 +55,7 @@ export const LOG_STYLES: string = `
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #00bfff;
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
.custom-lines-button {
|
||||
|
@ -1,11 +1,18 @@
|
||||
export const REPLICATION_STYLE: string = `
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
.option-left {
|
||||
padding-left: 16px;
|
||||
margin-top: 24px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.option-left-down {
|
||||
|
@ -1,21 +1,21 @@
|
||||
export const REPLICATION_TEMPLATE: string = `
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="row flex-items-xs-between" style="height:24px;">
|
||||
<div class="flex-xs-middle option-left">
|
||||
<button *ngIf="projectId" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
|
||||
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
|
||||
</div>
|
||||
<div class="flex-xs-middle option-right">
|
||||
<div class="select" style="float: left;">
|
||||
<div class="select" style="float: left; top: 9px;">
|
||||
<select (change)="doFilterRuleStatus($event)">
|
||||
<option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<hbr-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchRules($event)" [currentValue]="search.ruleName"></hbr-filter>
|
||||
<a href="javascript:void(0)" (click)="refreshRules()">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchRules($event)" [currentValue]="search.ruleName"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refreshRules()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -23,14 +23,14 @@ export const REPLICATION_TEMPLATE: string = `
|
||||
<hbr-list-replication-rule #listReplicationRule [projectId]="projectId" (selectOne)="selectOneRule($event)" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
|
||||
</div>
|
||||
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="row flex-items-xs-between" style="height:60px;">
|
||||
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
|
||||
<div class="flex-items-xs-bottom option-right-down">
|
||||
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</button>
|
||||
<hbr-filter filterPlaceholder='{{"REPLICATION.FILTER_JOBS_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)" [currentValue]="search.repoName" ></hbr-filter>
|
||||
<a href="javascript:void(0)" (click)="refreshJobs()">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_JOBS_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)" [currentValue]="search.repoName" ></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refreshJobs()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-items-xs-right option-right" [hidden]="currentJobSearchOption === 0">
|
||||
@ -53,6 +53,7 @@ export const REPLICATION_TEMPLATE: string = `
|
||||
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="updateTimeComparator">{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'REPLICATION.JOB_PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let j of jobs" [clrDgItem]='j'>
|
||||
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.status}}</clr-dg-cell>
|
||||
|
@ -1,14 +1,17 @@
|
||||
export const REPOSITORY_STACKVIEW_STYLES: string = `
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sub-grid-custom {
|
||||
position: relative;
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
:host >>> .datagrid .datagrid-body .datagrid-row {
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
|
@ -1,11 +1,11 @@
|
||||
export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="height: 24px;">
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-filter filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
|
||||
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -21,7 +21,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
|
||||
<clr-dg-cell>{{r.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" class="sub-grid-custom" [repoName]="r.name" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
|
||||
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
||||
@ -31,4 +31,6 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
</div>
|
||||
`;
|
@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
@ -17,18 +17,15 @@ import { SystemInfoService, SystemInfoDefaultService } from '../service/system-i
|
||||
|
||||
import { click } from '../utils';
|
||||
|
||||
describe('RepositoryComponentStackview (inline template)', ()=> {
|
||||
|
||||
describe('RepositoryComponentStackview (inline template)', () => {
|
||||
|
||||
let compRepo: RepositoryStackviewComponent;
|
||||
let fixtureRepo: ComponentFixture<RepositoryStackviewComponent>;
|
||||
let repositoryService: RepositoryService;
|
||||
let spyRepos: jasmine.Spy;
|
||||
|
||||
let compTag: TagComponent;
|
||||
let fixtureTag: ComponentFixture<TagComponent>;
|
||||
let tagService: TagService;
|
||||
let systemInfoService: SystemInfoService;
|
||||
|
||||
let spyRepos: jasmine.Spy;
|
||||
let spyTags: jasmine.Spy;
|
||||
let spySystemInfo: jasmine.Spy;
|
||||
|
||||
@ -44,27 +41,26 @@ describe('RepositoryComponentStackview (inline template)', ()=> {
|
||||
"harbor_version": "v1.1.1-rc1-160-g565110d"
|
||||
};
|
||||
|
||||
|
||||
let mockRepoData: Repository[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "library/busybox",
|
||||
"project_id": 1,
|
||||
"description": "",
|
||||
"pull_count": 0,
|
||||
"star_count": 0,
|
||||
"tags_count": 1
|
||||
"id": 1,
|
||||
"name": "library/busybox",
|
||||
"project_id": 1,
|
||||
"description": "",
|
||||
"pull_count": 0,
|
||||
"star_count": 0,
|
||||
"tags_count": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "library/nginx",
|
||||
"project_id": 1,
|
||||
"description": "",
|
||||
"pull_count": 0,
|
||||
"star_count": 0,
|
||||
"tags_count": 1
|
||||
"id": 2,
|
||||
"name": "library/nginx",
|
||||
"project_id": 1,
|
||||
"description": "",
|
||||
"pull_count": 0,
|
||||
"star_count": 0,
|
||||
"tags_count": 1
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
let mockTagData: Tag[] = [
|
||||
{
|
||||
@ -80,10 +76,12 @@ describe('RepositoryComponentStackview (inline template)', ()=> {
|
||||
];
|
||||
|
||||
let config: IServiceConfig = {
|
||||
repositoryBaseEndpoint: '/api/repository/testing'
|
||||
repositoryBaseEndpoint: '/api/repository/testing',
|
||||
systemInfoEndpoint: '/api/systeminfo/testing',
|
||||
targetBaseEndpoint: '/api/tag/testing'
|
||||
};
|
||||
|
||||
beforeEach(async(()=>{
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule
|
||||
@ -96,7 +94,7 @@ describe('RepositoryComponentStackview (inline template)', ()=> {
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue : config },
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||
{ provide: TagService, useClass: TagDefaultService },
|
||||
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }
|
||||
@ -104,69 +102,74 @@ describe('RepositoryComponentStackview (inline template)', ()=> {
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(()=>{
|
||||
beforeEach(() => {
|
||||
fixtureRepo = TestBed.createComponent(RepositoryStackviewComponent);
|
||||
compRepo = fixtureRepo.componentInstance;
|
||||
compRepo.projectId = 1;
|
||||
compRepo.hasProjectAdminRole = true;
|
||||
|
||||
repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService);
|
||||
systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService);
|
||||
|
||||
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepoData));
|
||||
fixtureRepo.detectChanges();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
fixtureTag = TestBed.createComponent(TagComponent);
|
||||
compTag = fixtureTag.componentInstance;
|
||||
compTag.projectId = compRepo.projectId;
|
||||
compTag.repoName = 'library/busybox';
|
||||
compTag.hasProjectAdminRole = true;
|
||||
compTag.hasSignedIn = true;
|
||||
tagService = fixtureTag.debugElement.injector.get(TagService);
|
||||
systemInfoService = fixtureTag.debugElement.injector.get(SystemInfoService);
|
||||
spyTags = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTagData));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||
fixtureTag.detectChanges();
|
||||
fixtureRepo.detectChanges();
|
||||
});
|
||||
|
||||
it('should load and render data', async(()=>{
|
||||
it('should create', () => {
|
||||
expect(compRepo).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should load and render data', async(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
fixtureRepo.whenStable().then(()=>{
|
||||
|
||||
fixtureRepo.whenStable().then(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell'));
|
||||
fixtureRepo.detectChanges();
|
||||
expect(deRepo).toBeTruthy();
|
||||
let elRepo: HTMLElement = deRepo.nativeElement;
|
||||
fixtureRepo.detectChanges();
|
||||
expect(elRepo).toBeTruthy();
|
||||
fixtureRepo.detectChanges();
|
||||
expect(elRepo.textContent).toEqual('library/busybox');
|
||||
click(deRepo);
|
||||
fixtureTag.detectChanges();
|
||||
let deTag: DebugElement = fixtureTag.debugElement.query(By.css('datagrid-cell'));
|
||||
expect(deTag).toBeTruthy();
|
||||
let elTag: HTMLElement = deTag.nativeElement;
|
||||
expect(elTag).toBeTruthy();
|
||||
expect(elTag.textContent).toEqual('1.12.5');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should filter data by keyword', async(()=>{
|
||||
it('should filter data by keyword', async(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
fixtureRepo.whenStable().then(()=>{
|
||||
|
||||
fixtureRepo.whenStable().then(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
compRepo.doSearchRepoNames('nginx');
|
||||
fixtureRepo.detectChanges();
|
||||
let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('datagrid-cell'));
|
||||
fixtureRepo.detectChanges();
|
||||
expect(de).toBeTruthy();
|
||||
expect(de.length).toEqual(1);
|
||||
let el: HTMLElement = de[0].nativeElement;
|
||||
fixtureRepo.detectChanges();
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toEqual('library/nginx');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display embedded tag view when click >', async(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
fixtureRepo.whenStable().then(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixtureRepo.nativeElement.querySelector('.datagrid-expandable-caret');
|
||||
expect(el).toBeTruthy();
|
||||
let button: HTMLButtonElement = el.querySelector('button');
|
||||
expect(button).toBeTruthy();
|
||||
click(button);
|
||||
|
||||
fixtureRepo.detectChanges();
|
||||
let el2: HTMLElement = fixtureRepo.nativeElement.querySelector('.datagrid-row-detail');
|
||||
expect(el2).toBeTruthy();
|
||||
let el3: Element = el2.querySelector(".datagrid-cell");
|
||||
expect(el3).toBeTruthy();
|
||||
expect(el3.textContent).toEqual('1.11.5');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
@ -1,13 +1,26 @@
|
||||
import { Component, Input, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
Output,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Comparator } from 'clarity-angular';
|
||||
|
||||
import { REPOSITORY_STACKVIEW_TEMPLATE } from './repository-stackview.component.html';
|
||||
import { REPOSITORY_STACKVIEW_STYLES } from './repository-stackview.component.css';
|
||||
|
||||
import { Repository } from '../service/interface';
|
||||
import {
|
||||
Repository,
|
||||
SystemInfo,
|
||||
SystemInfoService,
|
||||
RepositoryService
|
||||
} from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { RepositoryService } from '../service/repository.service';
|
||||
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
|
||||
@ -17,11 +30,12 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Tag } from '../service/interface';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-repository-stackview',
|
||||
template: REPOSITORY_STACKVIEW_TEMPLATE,
|
||||
styles: [ REPOSITORY_STACKVIEW_STYLES ],
|
||||
styles: [REPOSITORY_STACKVIEW_STYLES],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RepositoryStackviewComponent implements OnInit {
|
||||
@ -30,23 +44,34 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Output() tagClickEvent = new EventEmitter<Tag>();
|
||||
|
||||
lastFilteredRepoName: string;
|
||||
repositories: Repository[];
|
||||
systemInfo: SystemInfo;
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
|
||||
pullCountComparator: Comparator<Repository> = new CustomComparator<Repository>('pull_count', 'number');
|
||||
|
||||
|
||||
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private translateService: TranslateService,
|
||||
private repositoryService: RepositoryService,
|
||||
private ref: ChangeDetectorRef){}
|
||||
|
||||
private systemInfoService: SystemInfoService,
|
||||
private ref: ChangeDetectorRef) { }
|
||||
|
||||
public get registryUrl(): string {
|
||||
return this.systemInfo ? this.systemInfo.registry_url : "";
|
||||
}
|
||||
|
||||
public get withNotary(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_notary : false;
|
||||
}
|
||||
|
||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||
if (message &&
|
||||
message.source === ConfirmationTargets.REPOSITORY &&
|
||||
@ -55,19 +80,24 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
toPromise<number>(this.repositoryService
|
||||
.deleteRepository(repoName))
|
||||
.then(
|
||||
response => {
|
||||
this.refresh();
|
||||
this.translateService.get('REPOSITORY.DELETED_REPO_SUCCESS')
|
||||
.subscribe(res=>this.errorHandler.info(res));
|
||||
response => {
|
||||
this.refresh();
|
||||
this.translateService.get('REPOSITORY.DELETED_REPO_SUCCESS')
|
||||
.subscribe(res => this.errorHandler.info(res));
|
||||
}).catch(error => this.errorHandler.error(error));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if(!this.projectId) {
|
||||
if (!this.projectId) {
|
||||
this.errorHandler.error('Project ID cannot be unset.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
//Get system info for tag views
|
||||
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
|
||||
.then(systemInfo => this.systemInfo = systemInfo)
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
|
||||
this.lastFilteredRepoName = '';
|
||||
this.retrieve();
|
||||
}
|
||||
@ -76,10 +106,10 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
toPromise<Repository[]>(this.repositoryService
|
||||
.getRepositories(this.projectId, this.lastFilteredRepoName))
|
||||
.then(
|
||||
repos => this.repositories = repos,
|
||||
error => this.errorHandler.error(error));
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
repos => this.repositories = repos,
|
||||
error => this.errorHandler.error(error));
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
doSearchRepoNames(repoName: string) {
|
||||
@ -101,4 +131,8 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
refresh() {
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
watchTagClickEvt(tag: Tag): void {
|
||||
this.tagClickEvent.emit(tag);
|
||||
}
|
||||
}
|
@ -45,6 +45,7 @@ export interface Tag extends Base {
|
||||
author: string;
|
||||
created: Date;
|
||||
signature?: string;
|
||||
vulnerability?: VulnerabilitySummary;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -157,28 +158,28 @@ export interface SystemInfo {
|
||||
|
||||
//Not finalized yet
|
||||
export enum VulnerabilitySeverity {
|
||||
LOW, MEDIUM, HIGH, UNKNOWN, NONE
|
||||
NONE, UNKNOWN, LOW, MEDIUM, HIGH
|
||||
}
|
||||
|
||||
export interface ScanningBaseResult {
|
||||
export interface VulnerabilityBase {
|
||||
id: string;
|
||||
severity: VulnerabilitySeverity;
|
||||
package: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ScanningDetailResult extends ScanningBaseResult {
|
||||
export interface VulnerabilityItem extends VulnerabilityBase {
|
||||
fixedVersion: string;
|
||||
layer: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ScanningResultSummary {
|
||||
totalComponents: number;
|
||||
noneComponents: number;
|
||||
completeTimestamp: Date;
|
||||
high: ScanningBaseResult[];
|
||||
medium: ScanningBaseResult[];
|
||||
low: ScanningBaseResult[];
|
||||
unknown: ScanningBaseResult[];
|
||||
export interface VulnerabilitySummary {
|
||||
total_package: number;
|
||||
package_with_none: number;
|
||||
package_with_high?: number;
|
||||
package_with_medium?: number;
|
||||
package_With_low?: number;
|
||||
package_with_unknown?: number;
|
||||
complete_timestamp: Date;
|
||||
}
|
@ -5,8 +5,10 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { Http, URLSearchParams } from '@angular/http';
|
||||
import { HTTP_JSON_OPTIONS } from '../utils';
|
||||
|
||||
import { ScanningDetailResult } from './interface';
|
||||
import { VulnerabilitySeverity, ScanningBaseResult, ScanningResultSummary } from './interface';
|
||||
import {
|
||||
VulnerabilityItem,
|
||||
VulnerabilitySummary
|
||||
} from './interface';
|
||||
|
||||
/**
|
||||
* Get the vulnerabilities scanning results for the specified tag.
|
||||
@ -21,22 +23,22 @@ export abstract class ScanningResultService {
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary)}
|
||||
* @returns {(Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary)}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
abstract getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary;
|
||||
abstract getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
|
||||
|
||||
/**
|
||||
* Get the detailed vulnerabilities scanning results.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[])}
|
||||
* @returns {(Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[])}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
abstract getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[];
|
||||
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -47,7 +49,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
|
||||
super();
|
||||
}
|
||||
|
||||
getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary {
|
||||
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
|
||||
if (!tagId || tagId.trim() === '') {
|
||||
return Promise.reject('Bad argument');
|
||||
}
|
||||
@ -55,7 +57,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[] {
|
||||
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
|
||||
if (!tagId || tagId.trim() === '') {
|
||||
return Promise.reject('Bad argument');
|
||||
}
|
||||
|
@ -52,7 +52,19 @@ export abstract class TagService {
|
||||
*
|
||||
* @memberOf TagService
|
||||
*/
|
||||
abstract deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<Tag> | any;
|
||||
abstract deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<any> | any;
|
||||
|
||||
/**
|
||||
* Get the specified tag.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repositoryName
|
||||
* @param {string} tag
|
||||
* @returns {(Observable<Tag> | Promise<Tag> | Tag)}
|
||||
*
|
||||
* @memberOf TagService
|
||||
*/
|
||||
abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,4 +125,15 @@ export class TagDefaultService extends TagService {
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag {
|
||||
if (!repositoryName || !tag) {
|
||||
return Promise.reject("Bad argument");
|
||||
}
|
||||
|
||||
let url: string = `${this._baseUrl}/${repositoryName}/tags/${tag}`;
|
||||
return this.http.get(url, HTTP_JSON_OPTIONS).toPromise()
|
||||
.then(response => response.json() as Tag)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
import { Type } from '@angular/core';
|
||||
import { TagComponent } from './tag.component';
|
||||
import { TagDetailComponent } from './tag-detail.component';
|
||||
|
||||
export * from './tag.component';
|
||||
export * from './tag-detail.component';
|
||||
|
||||
export const TAG_DIRECTIVES: Type<any>[] = [
|
||||
TagComponent
|
||||
TagComponent,
|
||||
TagDetailComponent
|
||||
];
|
109
src/ui_ng/lib/src/tag/tag-detail.component.css.ts
Normal file
109
src/ui_ng/lib/src/tag/tag-detail.component.css.ts
Normal file
@ -0,0 +1,109 @@
|
||||
export const TAG_DETAIL_STYLES: string = `
|
||||
.overview-section {
|
||||
background-color: white;
|
||||
padding-bottom: 36px;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background-color: #fafafa;
|
||||
padding-left: 12px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.tag-name {
|
||||
font-weight: 300;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.tag-timestamp {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.rotate-90 {
|
||||
-webkit-transform: rotate(-90deg);
|
||||
/*Firefox*/
|
||||
-moz-transform: rotate(-90deg);
|
||||
/*Chrome*/
|
||||
-ms-transform: rotate(-90deg);
|
||||
/*IE9 、IE10*/
|
||||
-o-transform: rotate(-90deg);
|
||||
/*Opera*/
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.arrow-back {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow-block {
|
||||
border-right: 2px solid #cccccc;
|
||||
margin-right: 6px;
|
||||
display: inline-flex;
|
||||
padding: 6px 6px 6px 12px;
|
||||
}
|
||||
|
||||
.vulnerability-block {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary-block {
|
||||
margin-top: 24px;
|
||||
display: inline-flex;
|
||||
flex-wrap: row wrap;
|
||||
}
|
||||
|
||||
.image-summary {
|
||||
margin-right: 36px;
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
.flex-block {
|
||||
display: inline-flex;
|
||||
flex-wrap: row wrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.vulnerabilities-info {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.vulnerabilities-info .third-column {
|
||||
margin-left: 36px;
|
||||
}
|
||||
|
||||
.vulnerabilities-info .second-column,
|
||||
.vulnerabilities-info .fourth-column {
|
||||
text-align: left;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.vulnerabilities-info .second-row {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.image-detail-label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.image-detail-value {
|
||||
text-align: left;
|
||||
margin-left: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
77
src/ui_ng/lib/src/tag/tag-detail.component.html.ts
Normal file
77
src/ui_ng/lib/src/tag/tag-detail.component.html.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export const TAG_DETAIL_HTML: string = `
|
||||
<div>
|
||||
<section class="overview-section">
|
||||
<div class="title-wrapper">
|
||||
<div class="title-block arrow-block">
|
||||
<clr-icon class="rotate-90 arrow-back" shape="arrow" size="36" (click)="onBack()"></clr-icon>
|
||||
</div>
|
||||
<div class="title-block">
|
||||
<div class="tag-name">
|
||||
{{tagDetails.name}}:v{{tagDetails.docker_version}}
|
||||
</div>
|
||||
<div class="tag-timestamp">
|
||||
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-block">
|
||||
<div class="image-summary">
|
||||
<div class="detail-title">
|
||||
{{'TAG.IMAGE_DETAILS' | translate }}
|
||||
</div>
|
||||
<div class="flex-block">
|
||||
<div class="image-detail-label">
|
||||
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
|
||||
<div>{{'TAG.OS' | translate }}</div>
|
||||
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
|
||||
</div>
|
||||
<div class="image-detail-value">
|
||||
<div>{{tagDetails.architecture}}</div>
|
||||
<div>{{tagDetails.os}}</div>
|
||||
<div>{{scanCompletedDatetime | date}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="detail-title">
|
||||
{{'TAG.IMAGE_VULNERABILITIES' | translate }}
|
||||
</div>
|
||||
<div class="flex-block vulnerabilities-info">
|
||||
<div>
|
||||
<div>
|
||||
<clr-icon shape="error" size="24" class="is-error"></clr-icon>
|
||||
</div>
|
||||
<div class="second-row">
|
||||
<clr-icon shape="exclamation-triangle" size="24" class="is-warning"></clr-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="second-column">
|
||||
<div>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}</div>
|
||||
<div class="second-row">{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}</div>
|
||||
</div>
|
||||
<div class="third-column">
|
||||
<div>
|
||||
<clr-icon shape="play" size="20" class="is-warning rotate-90"></clr-icon>
|
||||
</div>
|
||||
<div class="second-row">
|
||||
<clr-icon shape="help" size="20"></clr-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fourth-column">
|
||||
<div>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}</div>
|
||||
<div class="second-row">{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<div class="vulnerability-block">
|
||||
<hbr-vulnerabilities-grid tagId="tagId"></hbr-vulnerabilities-grid>
|
||||
</div>
|
||||
<div>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
120
src/ui_ng/lib/src/tag/tag-detail.component.spec.ts
Normal file
120
src/ui_ng/lib/src/tag/tag-detail.component.spec.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ResultGridComponent } from '../vulnerability-scanning/result-grid.component';
|
||||
import { TagDetailComponent } from './tag-detail.component';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { Tag, VulnerabilitySummary } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
||||
import { FilterComponent } from '../filter/index';
|
||||
|
||||
describe('TagDetailComponent (inline template)', () => {
|
||||
|
||||
let comp: TagDetailComponent;
|
||||
let fixture: ComponentFixture<TagDetailComponent>;
|
||||
let tagService: TagService;
|
||||
let spy: jasmine.Spy;
|
||||
let mockVulnerability: VulnerabilitySummary = {
|
||||
total_package: 124,
|
||||
package_with_none: 92,
|
||||
package_with_high: 10,
|
||||
package_with_medium: 6,
|
||||
package_With_low: 13,
|
||||
package_with_unknown: 3,
|
||||
complete_timestamp: new Date()
|
||||
};
|
||||
let mockTag: Tag = {
|
||||
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||
"name": "nginx",
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"docker_version": "1.12.3",
|
||||
"author": "steven",
|
||||
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||
"signature": null,
|
||||
vulnerability: mockVulnerability
|
||||
};
|
||||
|
||||
let config: IServiceConfig = {
|
||||
repositoryBaseEndpoint: '/api/repositories/testing'
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
TagDetailComponent,
|
||||
ResultGridComponent,
|
||||
FilterComponent
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: TagService, useClass: TagDefaultService },
|
||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
||||
]
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TagDetailComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
comp.tagId = "mock_tag";
|
||||
comp.repositoryId = "mock_repo";
|
||||
|
||||
tagService = fixture.debugElement.injector.get(TagService);
|
||||
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should load data', async(() => {
|
||||
expect(spy.calls.any).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should rightly display tag name and version', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('nginx:v1.12.3');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display tag details', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.image-detail-value');
|
||||
expect(el).toBeTruthy();
|
||||
let el2: HTMLElement = el.querySelector('div');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.textContent).toEqual("amd64");
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display vulnerability details', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.second-column');
|
||||
expect(el).toBeTruthy();
|
||||
let el2: HTMLElement = el.querySelector('div');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.textContent.trim()).toEqual("10 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
88
src/ui_ng/lib/src/tag/tag-detail.component.ts
Normal file
88
src/ui_ng/lib/src/tag/tag-detail.component.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
|
||||
import { TAG_DETAIL_STYLES } from './tag-detail.component.css';
|
||||
import { TAG_DETAIL_HTML } from './tag-detail.component.html';
|
||||
|
||||
import { TagService, Tag } from '../service/index';
|
||||
import { toPromise } from '../utils';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-tag-detail',
|
||||
styles: [TAG_DETAIL_STYLES],
|
||||
template: TAG_DETAIL_HTML,
|
||||
|
||||
providers: []
|
||||
})
|
||||
export class TagDetailComponent implements OnInit {
|
||||
@Input() tagId: string;
|
||||
@Input() repositoryId: string;
|
||||
tagDetails: Tag = {
|
||||
name: "--",
|
||||
author: "--",
|
||||
created: new Date(),
|
||||
architecture: "--",
|
||||
os: "--",
|
||||
docker_version: "--",
|
||||
digest: "--"
|
||||
};
|
||||
|
||||
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
constructor(
|
||||
private tagService: TagService,
|
||||
private errorHandler: ErrorHandler) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.repositoryId && this.tagId) {
|
||||
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
|
||||
.then(response => this.tagDetails = response)
|
||||
.catch(error => this.errorHandler.error(error))
|
||||
}
|
||||
}
|
||||
|
||||
onBack(): void {
|
||||
this.backEvt.emit(this.tagId);
|
||||
}
|
||||
|
||||
public get highCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_with_high : 0;
|
||||
}
|
||||
|
||||
public get mediumCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_with_medium : 0;
|
||||
}
|
||||
|
||||
public get lowCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_With_low : 0;
|
||||
}
|
||||
|
||||
public get unknownCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_with_unknown : 0;
|
||||
}
|
||||
|
||||
public get scanCompletedDatetime(): Date {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.complete_timestamp : new Date();
|
||||
}
|
||||
|
||||
public get suffixForHigh(): string {
|
||||
return this.highCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||
}
|
||||
|
||||
public get suffixForMedium(): string {
|
||||
return this.mediumCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||
}
|
||||
|
||||
public get suffixForLow(): string {
|
||||
return this.lowCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||
}
|
||||
|
||||
public get suffixForUnknown(): string {
|
||||
return this.unknownCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||
}
|
||||
}
|
@ -22,12 +22,13 @@ export const TAG_TEMPLATE = `
|
||||
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'architecture'">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell><a href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a></clr-dg-cell>
|
||||
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
|
||||
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>
|
||||
|
@ -8,33 +8,16 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { TagComponent } from './tag.component';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { SystemInfo, Tag } from '../service/interface';
|
||||
import { Tag } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||
|
||||
describe('TagComponent (inline template)', ()=> {
|
||||
|
||||
let comp: TagComponent;
|
||||
let fixture: ComponentFixture<TagComponent>;
|
||||
let tagService: TagService;
|
||||
let systemInfoService: SystemInfoService;
|
||||
let spy: jasmine.Spy;
|
||||
let spySystemInfo: jasmine.Spy;
|
||||
|
||||
|
||||
let mockSystemInfo: SystemInfo = {
|
||||
"with_notary": true,
|
||||
"with_admiral": false,
|
||||
"admiral_endpoint": "NA",
|
||||
"auth_mode": "db_auth",
|
||||
"registry_url": "10.112.122.56",
|
||||
"project_creation_restriction": "everyone",
|
||||
"self_registration": true,
|
||||
"has_ca_root": false,
|
||||
"harbor_version": "v1.1.1-rc1-160-g565110d"
|
||||
};
|
||||
|
||||
let mockTags: Tag[] = [
|
||||
{
|
||||
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||
@ -64,8 +47,7 @@ describe('TagComponent (inline template)', ()=> {
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: TagService, useClass: TagDefaultService },
|
||||
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }
|
||||
{ provide: TagService, useClass: TagDefaultService }
|
||||
]
|
||||
});
|
||||
}));
|
||||
@ -78,15 +60,15 @@ describe('TagComponent (inline template)', ()=> {
|
||||
comp.repoName = 'library/nginx';
|
||||
comp.hasProjectAdminRole = true;
|
||||
comp.hasSignedIn = true;
|
||||
comp.registryUrl = 'http://registry.testing.com';
|
||||
comp.withNotary = false;
|
||||
|
||||
tagService = fixture.debugElement.injector.get(TagService);
|
||||
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
|
||||
spy = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTags));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('Should load data', async(()=>{
|
||||
it('should load data', async(()=>{
|
||||
expect(spy.calls.any).toBeTruthy();
|
||||
}));
|
||||
|
||||
|
@ -14,7 +14,6 @@
|
||||
import { Component, OnInit, ViewChild, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
import { TagService } from '../service/tag.service';
|
||||
import { SystemInfoService } from '../service/system-info.service';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../shared/shared.const';
|
||||
@ -23,7 +22,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
|
||||
import { SystemInfo, Tag } from '../service/interface';
|
||||
import { Tag } from '../service/interface';
|
||||
|
||||
import { TAG_TEMPLATE } from './tag.component.html';
|
||||
import { TAG_STYLE } from './tag.component.css';
|
||||
@ -37,24 +36,25 @@ import { State, Comparator } from 'clarity-angular';
|
||||
@Component({
|
||||
selector: 'hbr-tag',
|
||||
template: TAG_TEMPLATE,
|
||||
styles: [ TAG_STYLE ],
|
||||
styles: [TAG_STYLE],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TagComponent implements OnInit {
|
||||
|
||||
@Input() projectId: number;
|
||||
@Input() repoName: string;
|
||||
@Input() isEmbedded: boolean;
|
||||
@Input() isEmbedded: boolean;
|
||||
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Input() registryUrl: string;
|
||||
@Input() withNotary: boolean;
|
||||
|
||||
@Output() refreshRepo = new EventEmitter<boolean>();
|
||||
@Output() tagClickEvent = new EventEmitter<Tag>();
|
||||
|
||||
tags: Tag[];
|
||||
|
||||
registryUrl: string;
|
||||
withNotary: boolean;
|
||||
|
||||
showTagManifestOpened: boolean;
|
||||
manifestInfoTitle: string;
|
||||
@ -71,10 +71,9 @@ export class TagComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private systemInfoService: SystemInfoService,
|
||||
private tagService: TagService,
|
||||
private translateService: TranslateService,
|
||||
private ref: ChangeDetectorRef){}
|
||||
private ref: ChangeDetectorRef) { }
|
||||
|
||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||
if (message &&
|
||||
@ -86,35 +85,28 @@ export class TagComponent implements OnInit {
|
||||
return;
|
||||
} else {
|
||||
toPromise<number>(this.tagService
|
||||
.deleteTag(this.repoName, tag.name))
|
||||
.then(
|
||||
response => {
|
||||
.deleteTag(this.repoName, tag.name))
|
||||
.then(
|
||||
response => {
|
||||
this.retrieve();
|
||||
this.translateService.get('REPOSITORY.DELETED_TAG_SUCCESS')
|
||||
.subscribe(res=>this.errorHandler.info(res));
|
||||
}).catch(error => this.errorHandler.error(error));
|
||||
.subscribe(res => this.errorHandler.info(res));
|
||||
}).catch(error => this.errorHandler.error(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if(!this.projectId) {
|
||||
if (!this.projectId) {
|
||||
this.errorHandler.error('Project ID cannot be unset.');
|
||||
return;
|
||||
}
|
||||
if(!this.repoName) {
|
||||
if (!this.repoName) {
|
||||
this.errorHandler.error('Repo name cannot be unset.');
|
||||
return;
|
||||
}
|
||||
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
|
||||
.then(systemInfo=>{
|
||||
if(systemInfo) {
|
||||
this.registryUrl = systemInfo.registry_url || '';
|
||||
this.withNotary = systemInfo.with_notary || false;
|
||||
}
|
||||
},
|
||||
error=> this.errorHandler.error(error));
|
||||
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
@ -122,20 +114,20 @@ export class TagComponent implements OnInit {
|
||||
this.tags = [];
|
||||
this.loading = true;
|
||||
toPromise<Tag[]>(this.tagService
|
||||
.getTags(this.repoName))
|
||||
.then(items => {
|
||||
this.tags = items;
|
||||
this.loading = false;
|
||||
if(this.tags && this.tags.length === 0) {
|
||||
this.refreshRepo.emit(true);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.errorHandler.error(error);
|
||||
this.loading = false;
|
||||
});
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
.getTags(this.repoName))
|
||||
.then(items => {
|
||||
this.tags = items;
|
||||
this.loading = false;
|
||||
if (this.tags && this.tags.length === 0) {
|
||||
this.refreshRepo.emit(true);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.errorHandler.error(error);
|
||||
this.loading = false;
|
||||
});
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
deleteTag(tag: Tag) {
|
||||
@ -164,13 +156,20 @@ export class TagComponent implements OnInit {
|
||||
}
|
||||
|
||||
showDigestId(tag: Tag) {
|
||||
if(tag) {
|
||||
if (tag) {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
|
||||
this.digestId = tag.digest;
|
||||
this.showTagManifestOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
selectAndCopy($event: any) {
|
||||
$event.target.select();
|
||||
}
|
||||
|
||||
onTagClick(tag: Tag): void {
|
||||
if (tag) {
|
||||
this.tagClickEvent.emit(tag);
|
||||
}
|
||||
}
|
||||
}
|
@ -118,4 +118,9 @@ export class CustomComparator<T> implements Comparator<T> {
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default page size
|
||||
*/
|
||||
export const DEFAULT_PAGE_SIZE: number = 15;
|
@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ScanningResultSummary, VulnerabilitySeverity, ScanningBaseResult } from '../service/index';
|
||||
import { VulnerabilitySummary } from '../service/index';
|
||||
|
||||
import { ResultBarChartComponent, ScanState } from './result-bar-chart.component';
|
||||
import { ResultTipComponent } from './result-tip.component';
|
||||
@ -16,11 +16,18 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
let component: ResultBarChartComponent;
|
||||
let fixture: ComponentFixture<ResultBarChartComponent>;
|
||||
let serviceConfig: IServiceConfig;
|
||||
let scanningService: ScanningResultService;
|
||||
let spy: jasmine.Spy;
|
||||
let testConfig: IServiceConfig = {
|
||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||
};
|
||||
let mockData: VulnerabilitySummary = {
|
||||
total_package: 124,
|
||||
package_with_none: 92,
|
||||
package_with_high: 10,
|
||||
package_with_medium: 6,
|
||||
package_With_low: 13,
|
||||
package_with_unknown: 3,
|
||||
complete_timestamp: new Date()
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@ -32,8 +39,7 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
ResultTipComponent],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue: testConfig },
|
||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
||||
{ provide: SERVICE_CONFIG, useValue: testConfig }
|
||||
]
|
||||
});
|
||||
|
||||
@ -43,52 +49,9 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
fixture = TestBed.createComponent(ResultBarChartComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.tagId = "mockTag";
|
||||
component.state = ScanState.COMPLETED;
|
||||
component.state = ScanState.UNKNOWN;
|
||||
|
||||
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
||||
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
||||
let mockData: ScanningResultSummary = {
|
||||
totalComponents: 21,
|
||||
noneComponents: 7,
|
||||
completeTimestamp: new Date(),
|
||||
high: [],
|
||||
medium: [],
|
||||
low: [],
|
||||
unknown: []
|
||||
};
|
||||
|
||||
for (let i = 0; i < 14; i++) {
|
||||
let res: ScanningBaseResult = {
|
||||
id: "CVE-2016-" + (8859 + i),
|
||||
package: "package_" + i,
|
||||
version: '4.' + i + ".0",
|
||||
severity: VulnerabilitySeverity.UNKNOWN
|
||||
};
|
||||
|
||||
switch (i % 4) {
|
||||
case 0:
|
||||
res.severity = VulnerabilitySeverity.HIGH;
|
||||
mockData.high.push(res);
|
||||
break;
|
||||
case 1:
|
||||
res.severity = VulnerabilitySeverity.MEDIUM;
|
||||
mockData.medium.push(res);
|
||||
break;
|
||||
case 2:
|
||||
res.severity = VulnerabilitySeverity.LOW;
|
||||
mockData.low.push(res);
|
||||
break;
|
||||
case 3:
|
||||
res.severity = VulnerabilitySeverity.UNKNOWN;
|
||||
mockData.unknown.push(res);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
spy = spyOn(scanningService, 'getScanningResultSummary')
|
||||
.and.returnValue(Promise.resolve(mockData));
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@ -102,22 +65,57 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
|
||||
});
|
||||
|
||||
it('should inject and call the ScanningResultService', () => {
|
||||
expect(scanningService).toBeTruthy();
|
||||
expect(spy.calls.any()).toBe(true, 'getScanningResultSummary called');
|
||||
});
|
||||
|
||||
it('should get data from ScanningResultService', async(() => {
|
||||
it('should show a button if status is PENDING', async(() => {
|
||||
component.state = ScanState.PENDING;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||
fixture.detectChanges();
|
||||
expect(component.summary).toBeTruthy();
|
||||
expect(component.summary.totalComponents).toEqual(21);
|
||||
expect(component.summary.high.length).toEqual(4);
|
||||
expect(component.summary.medium.length).toEqual(4);
|
||||
expect(component.summary.low.length).toEqual(3);
|
||||
expect(component.summary.noneComponents).toEqual(7);
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.scanning-button');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should show progress if status is SCANNING', async(() => {
|
||||
component.state = ScanState.SCANNING;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.progress');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should show QUEUED if status is QUEUED', async(() => {
|
||||
component.state = ScanState.QUEUED;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-state');
|
||||
expect(el).toBeTruthy();
|
||||
let el2: HTMLElement = el.querySelector('span');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.textContent).toEqual('VULNERABILITY.STATE.QUEUED');
|
||||
|
||||
});
|
||||
}));
|
||||
|
||||
it('should show summary bar chart if status is COMPLETED', async(() => {
|
||||
component.state = ScanState.COMPLETED;
|
||||
component.summary = mockData;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.style.width).toEqual("74px");
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -2,16 +2,9 @@ import {
|
||||
Component,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
OnInit
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ScanningResultService,
|
||||
ScanningResultSummary
|
||||
} from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
import { toPromise } from '../utils';
|
||||
import { MAX_TIP_WIDTH } from './result-tip.component';
|
||||
import { VulnerabilitySummary } from '../service/index';
|
||||
import { SCANNING_STYLES } from './scanning.css';
|
||||
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
|
||||
|
||||
@ -25,37 +18,21 @@ export enum ScanState {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-scan-result-bar',
|
||||
selector: 'hbr-vulnerability-bar',
|
||||
styles: [SCANNING_STYLES],
|
||||
template: BAR_CHART_COMPONENT_HTML
|
||||
})
|
||||
export class ResultBarChartComponent implements OnInit {
|
||||
export class ResultBarChartComponent {
|
||||
@Input() tagId: string = "";
|
||||
@Input() state: ScanState = ScanState.UNKNOWN;
|
||||
@Input() summary: ScanningResultSummary = {
|
||||
totalComponents: 0,
|
||||
noneComponents: 0,
|
||||
completeTimestamp: new Date(),
|
||||
high: [],
|
||||
medium: [],
|
||||
low: [],
|
||||
unknown: []
|
||||
@Input() summary: VulnerabilitySummary = {
|
||||
total_package: 0,
|
||||
package_with_none: 0,
|
||||
complete_timestamp: new Date()
|
||||
};
|
||||
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
private scanningService: ScanningResultService,
|
||||
private errorHandler: ErrorHandler) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
toPromise<ScanningResultSummary>(this.scanningService.getScanningResultSummary(this.tagId))
|
||||
.then((summary: ScanningResultSummary) => {
|
||||
this.summary = summary;
|
||||
})
|
||||
.catch(error => {
|
||||
this.errorHandler.error(error);
|
||||
})
|
||||
}
|
||||
constructor() { }
|
||||
|
||||
public get completed(): boolean {
|
||||
return this.state === ScanState.COMPLETED;
|
||||
@ -86,66 +63,4 @@ export class ResultBarChartComponent implements OnInit {
|
||||
this.startScanning.emit(this.tagId);
|
||||
}
|
||||
}
|
||||
|
||||
public get hasHigh(): boolean {
|
||||
return this.summary && this.summary.high && this.summary.high.length > 0;
|
||||
}
|
||||
|
||||
public get hasMedium(): boolean {
|
||||
return this.summary && this.summary.medium && this.summary.medium.length > 0;
|
||||
}
|
||||
|
||||
public get hasLow(): boolean {
|
||||
return this.summary && this.summary.low && this.summary.low.length > 0;
|
||||
}
|
||||
|
||||
public get hasUnknown(): boolean {
|
||||
return this.summary && this.summary.unknown && this.summary.unknown.length > 0;
|
||||
}
|
||||
|
||||
public get hasNone(): boolean {
|
||||
return this.summary && this.summary.noneComponents > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the percent width of each severity.
|
||||
*
|
||||
* @param {string} flag
|
||||
* 'h': high
|
||||
* 'm': medium
|
||||
* 'l': low
|
||||
* 'u': unknown
|
||||
* 'n': none
|
||||
* @returns {number}
|
||||
*
|
||||
* @memberOf ResultBarChartComponent
|
||||
*/
|
||||
percent(flag: string): number {
|
||||
if (!this.summary || this.summary.totalComponents === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let numerator: number = 0;
|
||||
switch (flag) {
|
||||
case 'h':
|
||||
numerator = this.summary.high.length;
|
||||
break;
|
||||
case 'm':
|
||||
numerator = this.summary.medium.length;
|
||||
break;
|
||||
case 'l':
|
||||
numerator = this.summary.low.length;
|
||||
break;
|
||||
case 'u':
|
||||
numerator = this.summary.unknown.length;
|
||||
break;
|
||||
default:
|
||||
numerator = this.summary.noneComponents;
|
||||
break;
|
||||
}
|
||||
|
||||
return Math.round((numerator / this.summary.totalComponents) * MAX_TIP_WIDTH);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -3,13 +3,14 @@ import { By } from '@angular/platform-browser';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ScanningDetailResult, VulnerabilitySeverity, RequestQueryParams } from '../service/index';
|
||||
import { VulnerabilityItem, VulnerabilitySeverity, RequestQueryParams } from '../service/index';
|
||||
|
||||
import { ResultGridComponent } from './result-grid.component';
|
||||
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { FilterComponent } from '../filter/index';
|
||||
|
||||
describe('ResultGridComponent (inline template)', () => {
|
||||
let component: ResultGridComponent;
|
||||
@ -26,7 +27,7 @@ describe('ResultGridComponent (inline template)', () => {
|
||||
imports: [
|
||||
SharedModule
|
||||
],
|
||||
declarations: [ResultGridComponent],
|
||||
declarations: [ResultGridComponent, FilterComponent],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue: testConfig },
|
||||
@ -43,9 +44,9 @@ describe('ResultGridComponent (inline template)', () => {
|
||||
|
||||
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
||||
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
||||
let mockData: ScanningDetailResult[] = [];
|
||||
let mockData: VulnerabilityItem[] = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
let res: ScanningDetailResult = {
|
||||
let res: VulnerabilityItem = {
|
||||
id: "CVE-2016-" + (8859 + i),
|
||||
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
|
||||
package: "package_" + i,
|
||||
@ -57,7 +58,7 @@ describe('ResultGridComponent (inline template)', () => {
|
||||
mockData.push(res);
|
||||
}
|
||||
|
||||
spy = spyOn(scanningService, 'getScanningResults')
|
||||
spy = spyOn(scanningService, 'getVulnerabilityScanningResults')
|
||||
.and.returnValue(Promise.resolve(mockData));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import {
|
||||
ScanningResultService,
|
||||
ScanningDetailResult
|
||||
VulnerabilityItem
|
||||
} from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
|
||||
@ -10,12 +10,12 @@ import { GRID_COMPONENT_HTML } from './scanning.html';
|
||||
import { SCANNING_STYLES } from './scanning.css';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-scan-result-grid',
|
||||
selector: 'hbr-vulnerabilities-grid',
|
||||
styles: [SCANNING_STYLES],
|
||||
template: GRID_COMPONENT_HTML
|
||||
})
|
||||
export class ResultGridComponent implements OnInit {
|
||||
scanningResults: ScanningDetailResult[] = [];
|
||||
scanningResults: VulnerabilityItem[] = [];
|
||||
@Input() tagId: string;
|
||||
|
||||
constructor(
|
||||
@ -27,15 +27,23 @@ export class ResultGridComponent implements OnInit {
|
||||
this.loadResults(this.tagId);
|
||||
}
|
||||
|
||||
showDetail(result: ScanningDetailResult): void {
|
||||
showDetail(result: VulnerabilityItem): void {
|
||||
console.log(result.id);
|
||||
}
|
||||
|
||||
loadResults(tagId: string): void {
|
||||
toPromise<ScanningDetailResult[]>(this.scanningService.getScanningResults(tagId))
|
||||
.then((results: ScanningDetailResult[]) => {
|
||||
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(tagId))
|
||||
.then((results: VulnerabilityItem[]) => {
|
||||
this.scanningResults = results;
|
||||
})
|
||||
.catch(error => { this.errorHandler.error(error) })
|
||||
}
|
||||
|
||||
filterVulnerabilities(terms: string): void {
|
||||
console.log(terms);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.loadResults(this.tagId);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ScanningDetailResult, VulnerabilitySeverity } from '../service/index';
|
||||
import { VulnerabilitySummary } from '../service/index';
|
||||
|
||||
import { ResultTipComponent } from './result-tip.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
@ -16,6 +16,15 @@ describe('ResultTipComponent (inline template)', () => {
|
||||
let testConfig: IServiceConfig = {
|
||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||
};
|
||||
let mockData:VulnerabilitySummary = {
|
||||
total_package: 124,
|
||||
package_with_none: 90,
|
||||
package_with_high: 13,
|
||||
package_with_medium: 10,
|
||||
package_With_low: 10,
|
||||
package_with_unknown: 1,
|
||||
complete_timestamp: new Date()
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@ -31,14 +40,26 @@ describe('ResultTipComponent (inline template)', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ResultTipComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.percent = 50;
|
||||
component.summary = mockData;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.severity).toEqual(VulnerabilitySeverity.UNKNOWN);
|
||||
});
|
||||
|
||||
it('should reader the bar with different width', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.style.width).toEqual("73px");
|
||||
let el2: HTMLElement = fixture.nativeElement.querySelector('.bar-block-high');
|
||||
expect(el2).not.toBeNull();
|
||||
expect(el2.style.width).toEqual("10px");
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { VulnerabilitySummary, VulnerabilitySeverity } from '../service/index';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
ScanningBaseResult,
|
||||
VulnerabilitySeverity
|
||||
} from '../service/index';
|
||||
|
||||
import { SCANNING_STYLES } from './scanning.css';
|
||||
import { TIP_COMPONENT_HTML } from './scanning.html';
|
||||
|
||||
@ -11,127 +9,141 @@ export const MIN_TIP_WIDTH = 5;
|
||||
export const MAX_TIP_WIDTH = 100;
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-scan-result-tip',
|
||||
selector: 'hbr-vulnerability-summary-chart',
|
||||
template: TIP_COMPONENT_HTML,
|
||||
styles: [SCANNING_STYLES]
|
||||
})
|
||||
export class ResultTipComponent implements OnInit {
|
||||
_percent: number = 5;
|
||||
_tipTitle: string = '';
|
||||
_tipTitle: string = "";
|
||||
|
||||
@Input() severity: VulnerabilitySeverity = VulnerabilitySeverity.UNKNOWN;
|
||||
@Input() completeDateTime: Date = new Date(); //Temp
|
||||
@Input() data: ScanningBaseResult[] = [];
|
||||
@Input() noneNumber: number = 0;
|
||||
@Input()
|
||||
public get percent(): number {
|
||||
return this._percent;
|
||||
}
|
||||
@Input() summary: VulnerabilitySummary = {
|
||||
total_package: 0,
|
||||
package_with_none: 0,
|
||||
complete_timestamp: new Date()
|
||||
};
|
||||
|
||||
public set percent(percent: number) {
|
||||
this._percent = percent;
|
||||
if (this._percent < MIN_TIP_WIDTH) {
|
||||
this._percent = MIN_TIP_WIDTH;
|
||||
}
|
||||
|
||||
if (this._percent > MAX_TIP_WIDTH) {
|
||||
this._percent = MAX_TIP_WIDTH;
|
||||
}
|
||||
}
|
||||
|
||||
_getSeverityKey(): string {
|
||||
switch (this.severity) {
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
return 'VULNERABILITY.CHART.SEVERITY_HIGH';
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
return 'VULNERABILITY.CHART.SEVERITY_MEDIUM';
|
||||
case VulnerabilitySeverity.LOW:
|
||||
return 'VULNERABILITY.CHART.SEVERITY_LOW';
|
||||
case VulnerabilitySeverity.NONE:
|
||||
return 'VULNERABILITY.CHART.SEVERITY_NONE';
|
||||
default:
|
||||
return 'VULNERABILITY.CHART.SEVERITY_UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private translateService: TranslateService) { }
|
||||
constructor(private translate: TranslateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.translateService.get(this._getSeverityKey())
|
||||
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE',
|
||||
{ totalVulnerability: this.totalVulnerabilities, totalPackages: this.summary.total_package })
|
||||
.subscribe((res: string) => this._tipTitle = res);
|
||||
}
|
||||
|
||||
tipWidth(severity: VulnerabilitySeverity): string {
|
||||
let n: number = 0;
|
||||
let m: number = this.summary ? this.summary.total_package : 0;
|
||||
|
||||
if (m === 0) {
|
||||
return 0 + 'px';
|
||||
}
|
||||
|
||||
switch (severity) {
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
n = this.highCount;
|
||||
break;
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
n = this.mediumCount;
|
||||
break;
|
||||
case VulnerabilitySeverity.LOW:
|
||||
n = this.lowCount;
|
||||
break;
|
||||
case VulnerabilitySeverity.UNKNOWN:
|
||||
n = this.unknownCount;
|
||||
break;
|
||||
case VulnerabilitySeverity.NONE:
|
||||
n = this.noneCount;
|
||||
break;
|
||||
default:
|
||||
n = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
let width: number = Math.round((n/m)*MAX_TIP_WIDTH);
|
||||
if(width < MIN_TIP_WIDTH){
|
||||
width = MIN_TIP_WIDTH;
|
||||
}
|
||||
|
||||
return width + 'px';
|
||||
}
|
||||
|
||||
|
||||
unitText(count: number): string {
|
||||
if (count > 1) {
|
||||
return "VULNERABILITY.PLURAL";
|
||||
}
|
||||
|
||||
return "VULNERABILITY.SINGULAR";
|
||||
}
|
||||
|
||||
public get totalVulnerabilities(): number {
|
||||
return this.summary.total_package - this.summary.package_with_none;
|
||||
}
|
||||
|
||||
public get hasHigh(): boolean {
|
||||
return this.highCount > 0;
|
||||
}
|
||||
|
||||
public get hasMedium(): boolean {
|
||||
return this.mediumCount > 0;
|
||||
}
|
||||
|
||||
public get hasLow(): boolean {
|
||||
return this.lowCount > 0;
|
||||
}
|
||||
|
||||
public get hasUnknown(): boolean {
|
||||
return this.unknownCount > 0;
|
||||
}
|
||||
|
||||
public get hasNone(): boolean {
|
||||
return this.noneCount > 0;
|
||||
}
|
||||
|
||||
public get tipTitle(): string {
|
||||
if (!this.data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let dataSize: number = this.data.length;
|
||||
return this._tipTitle + ' (' + dataSize + ')';
|
||||
return this._tipTitle;
|
||||
}
|
||||
|
||||
public get hasResultsToList(): boolean {
|
||||
return this.data &&
|
||||
this.data.length > 0 && (
|
||||
this.severity !== VulnerabilitySeverity.NONE &&
|
||||
this.severity !== VulnerabilitySeverity.UNKNOWN
|
||||
);
|
||||
public get highCount(): number {
|
||||
return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0;
|
||||
}
|
||||
|
||||
public get tipWidth(): string {
|
||||
return this.percent + 'px';
|
||||
public get mediumCount(): number {
|
||||
return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0;
|
||||
}
|
||||
|
||||
public get tipClass(): string {
|
||||
let baseClass: string = "tip-wrapper tip-block";
|
||||
|
||||
switch (this.severity) {
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
return baseClass + " bar-block-high";
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
return baseClass + " bar-block-medium";
|
||||
case VulnerabilitySeverity.LOW:
|
||||
return baseClass + " bar-block-low";
|
||||
case VulnerabilitySeverity.NONE:
|
||||
return baseClass + " bar-block-none";
|
||||
default:
|
||||
return baseClass + " bar-block-unknown"
|
||||
}
|
||||
|
||||
public get lowCount(): number {
|
||||
return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0;
|
||||
}
|
||||
|
||||
public get isHigh(): boolean {
|
||||
return this.severity === VulnerabilitySeverity.HIGH;
|
||||
public get unknownCount(): number {
|
||||
return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0;
|
||||
}
|
||||
public get noneCount(): number {
|
||||
return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0;
|
||||
}
|
||||
|
||||
public get isMedium(): boolean {
|
||||
return this.severity === VulnerabilitySeverity.MEDIUM;
|
||||
public get highSuffix(): string {
|
||||
return this.unitText(this.highCount);
|
||||
}
|
||||
|
||||
public get isLow(): boolean {
|
||||
return this.severity === VulnerabilitySeverity.LOW;
|
||||
public get mediumSuffix(): string {
|
||||
return this.unitText(this.mediumCount);
|
||||
}
|
||||
|
||||
public get isNone(): boolean {
|
||||
return this.severity === VulnerabilitySeverity.NONE;
|
||||
public get lowSuffix(): string {
|
||||
return this.unitText(this.lowCount);
|
||||
}
|
||||
|
||||
public get isUnknown(): boolean {
|
||||
return this.severity === VulnerabilitySeverity.UNKNOWN;
|
||||
public get unknownSuffix(): string {
|
||||
return this.unitText(this.unknownCount);
|
||||
}
|
||||
|
||||
public get tipIconClass(): string {
|
||||
switch (this.severity) {
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
return "is-error";
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
return "is-warning";
|
||||
case VulnerabilitySeverity.LOW:
|
||||
return "is-info";
|
||||
case VulnerabilitySeverity.NONE:
|
||||
return "is-success";
|
||||
default:
|
||||
return "is-highlight"
|
||||
}
|
||||
public get noneSuffix(): string {
|
||||
return this.unitText(this.noneCount);
|
||||
}
|
||||
|
||||
public get maxWidth(): string {
|
||||
return MAX_TIP_WIDTH+"px";
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,9 @@ export const SCANNING_STYLES: string = `
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bar-state {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.scanning-button {
|
||||
height: 24px;
|
||||
margin-top: 0px;
|
||||
@ -17,65 +15,58 @@ export const SCANNING_STYLES: string = `
|
||||
top: -6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tip-wrapper {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
max-height: 16px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.tip-position {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.tip-block {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.bar-block-high {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.bar-block-medium {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.bar-block-low {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.bar-block-none {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.bar-block-unknown {
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.bar-tooltip-font {
|
||||
font-size: 13px;
|
||||
color: #565656;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.bar-tooltip-font-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar-summary {
|
||||
margin-top: 5px;
|
||||
margin-top: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bar-scanning-time {
|
||||
margin-left: 26px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bar-summary ul {
|
||||
margin-left: 24px;
|
||||
.bar-summary-item {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.bar-summary ul li {
|
||||
list-style-type: none;
|
||||
margin: 2px;
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
}
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
`;
|
@ -1,24 +1,40 @@
|
||||
export const TIP_COMPONENT_HTML: string = `
|
||||
<div class="tip-wrapper tip-position" [style.width]='tipWidth'>
|
||||
<div class="tip-wrapper tip-position" [style.width]='maxWidth'>
|
||||
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
|
||||
<div class="{{tipClass}}" [style.width]='tipWidth'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(4)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(3)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(2)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(1)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(0)'></div>
|
||||
<clr-tooltip-content>
|
||||
<div>
|
||||
<clr-icon *ngIf="isHigh" shape="exclamation-circle" class="{{tipIconClass}}" size="24"></clr-icon>
|
||||
<clr-icon *ngIf="isMedium" shape="exclamation-triangle" class="{{tipIconClass}}" size="24"></clr-icon>
|
||||
<clr-icon *ngIf="isLow" shape="info-circle" class="{{tipIconClass}}" size="24"></clr-icon>
|
||||
<clr-icon *ngIf="isNone" shape="check-circle" class="{{tipIconClass}}" size="24"></clr-icon>
|
||||
<clr-icon *ngIf="isUnknown" shape="help" class="{{tipIconClass}}" size="16"></clr-icon>
|
||||
<span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span>
|
||||
</div>
|
||||
<div class="bar-summary bar-tooltip-font">
|
||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||
<span>{{completeDateTime | date}}</span>
|
||||
<div *ngIf="hasResultsToList">
|
||||
<ul *ngFor="let item of data">
|
||||
<li>{{item.id}} {{item.version}} {{item.package}}</li>
|
||||
</ul>
|
||||
<div class="bar-summary bar-tooltip-fon">
|
||||
<div *ngIf="hasHigh" class="bar-summary-item">
|
||||
<clr-icon shape="exclamation-circle" class="is-error" size="24"></clr-icon>
|
||||
<span>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="hasMedium" class="bar-summary-item">
|
||||
<clr-icon *ngIf="hasMedium" shape="exclamation-triangle" class="is-warning" size="24"></clr-icon>
|
||||
<span>{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="hasLow" class="bar-summary-item">
|
||||
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||
<span>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="hasUnknown" class="bar-summary-item">
|
||||
<clr-icon shape="help" size="24"></clr-icon>
|
||||
<span>{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }}</span>
|
||||
</div>
|
||||
<div *ngIf="hasNone" class="bar-summary-item">
|
||||
<clr-icon shape="check-circle" class="is-success" size="24"></clr-icon>
|
||||
<span>{{noneCount}} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||
<span>{{summary.complete_timestamp | date}}</span>
|
||||
</div>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
@ -26,7 +42,16 @@ export const TIP_COMPONENT_HTML: string = `
|
||||
`;
|
||||
|
||||
export const GRID_COMPONENT_HTML: string = `
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="height: 24px;">
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'VULNERABILITY.PLACEHOLDER' | translate}}" (filter)="filterVulnerabilities($event)"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
|
||||
@ -55,6 +80,7 @@ export const GRID_COMPONENT_HTML: string = `
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults.length"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -75,11 +101,7 @@ export const BAR_CHART_COMPONENT_HTML: string = `
|
||||
<div class="progress loop" style="height:2px;min-height:2px;"><progress></progress></div>
|
||||
</div>
|
||||
<div *ngIf="completed" class="bar-state">
|
||||
<hbr-scan-result-tip *ngIf="hasHigh" [severity]="2" [completeDateTime]="summary.completeTimestamp" [data]="summary.high" [percent]='percent("h")'></hbr-scan-result-tip>
|
||||
<hbr-scan-result-tip *ngIf="hasMedium" [severity]="1" [completeDateTime]="summary.completeTimestamp" [data]="summary.medium" [percent]='percent("m")'></hbr-scan-result-tip>
|
||||
<hbr-scan-result-tip *ngIf="hasLow" [severity]="0" [completeDateTime]="summary.completeTimestamp" [data]="summary.low" [percent]='percent("l")'></hbr-scan-result-tip>
|
||||
<hbr-scan-result-tip *ngIf="hasUnknown" [severity]="3" [completeDateTime]="summary.completeTimestamp" [data]="summary.unknown" [percent]='percent("u")'></hbr-scan-result-tip>
|
||||
<hbr-scan-result-tip *ngIf="hasNone" [severity]="4" [completeDateTime]="summary.completeTimestamp" [noneNumber]="summary.noneComponents" [percent]='percent("n")'></hbr-scan-result-tip>
|
||||
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
|
||||
</div>
|
||||
<div *ngIf="unknown" class="bar-state">
|
||||
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
|
||||
|
@ -8,7 +8,8 @@
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor"
|
||||
"e2e": "protractor",
|
||||
"build": "ngc -p tsconfig-aot.json"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
@ -6,18 +6,18 @@
|
||||
<button [class.hide-create]="!canCreateUser" type="submit" class="btn btn-link custom-add-button" (click)="addNewUser()"><clr-icon shape="add"></clr-icon> {{'USER.ADD_ACTION' | translate}}</button>
|
||||
</span>
|
||||
<grid-filter class="filter-pos" filterPlaceholder='{{"USER.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></grid-filter>
|
||||
<span class="refresh-btn" (click)="refreshUser()">
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<clr-datagrid [clrDgLoading]="inProgress">
|
||||
<clr-datagrid (clrDgRefresh)="load($event)" [clrDgLoading]="inProgress">
|
||||
<clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'USER.COLUMN_REG_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let user of users" [clrDgItem]="user">
|
||||
<clr-dg-row *ngFor="let user of users" [clrDgItem]="user">
|
||||
<clr-dg-action-overflow [hidden]="isMySelf(user.user_id)">
|
||||
<button class="action-item" (click)="changeAdminRole(user)">{{adminActions(user)}}</button>
|
||||
<button class="action-item" (click)="deleteUser(user)">{{'USER.DEL_ACTION' | translate}}</button>
|
||||
@ -28,7 +28,7 @@
|
||||
<clr-dg-cell>{{user.creation_time | date: 'short'}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} users
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="15" [clrDgTotalItems]="users.length"> {{'USER.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="15" [(clrDgPage)]="currentPage" [clrDgTotalItems]="totalCount"> {{'USER.ITEMS' | translate}}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
|
@ -27,6 +27,16 @@ import { MessageHandlerService } from '../shared/message-handler/message-handler
|
||||
import { SessionService } from '../shared/session.service';
|
||||
import { AppConfigService } from '../app-config.service';
|
||||
|
||||
/**
|
||||
* NOTES:
|
||||
* Pagination for this component is a temporary workaround solution. It will be replaced in future release.
|
||||
*
|
||||
* @export
|
||||
* @class UserComponent
|
||||
* @implements {OnInit}
|
||||
* @implements {OnDestroy}
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'harbor-user',
|
||||
templateUrl: 'user.component.html',
|
||||
@ -44,6 +54,8 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
private deletionSubscription: Subscription;
|
||||
|
||||
currentTerm: string;
|
||||
totalCount: number = 0;
|
||||
currentPage: number = 1;
|
||||
|
||||
@ViewChild(NewUserModalComponent)
|
||||
newUserDialog: NewUserModalComponent;
|
||||
@ -63,8 +75,8 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
this.delUser(confirmed.data);
|
||||
}
|
||||
});
|
||||
let hnd = setInterval(()=>ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
let hnd = setInterval(() => ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
isMySelf(uid: number): boolean {
|
||||
@ -78,8 +90,8 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMatchFilterTerm(terms: string, testedItem: string): boolean {
|
||||
return testedItem.indexOf(terms) != -1;
|
||||
private isMatchFilterTerm(terms: string, testedItem: string): boolean {
|
||||
return testedItem.toLowerCase().indexOf(terms.toLowerCase()) != -1;
|
||||
}
|
||||
|
||||
public get canCreateUser(): boolean {
|
||||
@ -114,7 +126,6 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.refreshUser();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -128,15 +139,15 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
this.currentTerm = terms;
|
||||
this.originalUsers.then(users => {
|
||||
if (terms.trim() === "") {
|
||||
this.users = users;
|
||||
this.refreshUser((this.currentPage - 1) * 15, this.currentPage * 15);
|
||||
} else {
|
||||
this.users = users.filter(user => {
|
||||
return this.isMatchFilterTerm(terms, user.username);
|
||||
})
|
||||
}
|
||||
});
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
//Disable the admin role for the specified user
|
||||
@ -164,8 +175,8 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
.then(() => {
|
||||
//Change view now
|
||||
user.has_admin_role = updatedUser.has_admin_role;
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
this.msgHandler.handleError(error);
|
||||
@ -203,8 +214,8 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
this.originalUsers.then(users => {
|
||||
this.users = users.filter(u => u.user_id != user.user_id);
|
||||
this.msgHandler.showSuccess("USER.DELETE_SUCCESS");
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
@ -213,7 +224,7 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
//Refresh the user list
|
||||
refreshUser(): void {
|
||||
refreshUser(from: number, to: number): void {
|
||||
//Start to get
|
||||
this.currentTerm = '';
|
||||
this.onGoing = true;
|
||||
@ -221,15 +232,18 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
this.originalUsers = this.userService.getUsers()
|
||||
.then(users => {
|
||||
this.onGoing = false;
|
||||
this.users = users;
|
||||
|
||||
this.totalCount = users.length;
|
||||
this.users = users.slice(from, to);//First page
|
||||
|
||||
return users;
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
this.msgHandler.handleError(error);
|
||||
});
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
//Add new user
|
||||
@ -243,7 +257,28 @@ export class UserComponent implements OnInit, OnDestroy {
|
||||
//Add user to the user list
|
||||
addUserToList(user: User): void {
|
||||
//Currently we can only add it by reloading all
|
||||
this.refreshUser();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
//Data loading
|
||||
load(state: any): void {
|
||||
if (state && state.page) {
|
||||
if (this.originalUsers) {
|
||||
this.originalUsers.then(users => {
|
||||
this.users = users.slice(state.page.from, state.page.to + 1);
|
||||
});
|
||||
} else {
|
||||
this.refreshUser(state.page.from, state.page.to + 1);
|
||||
}
|
||||
} else {
|
||||
//Refresh
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.currentPage = 1;//Refresh pagination
|
||||
this.refreshUser(0, 15);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -436,6 +436,55 @@
|
||||
"IN_PROGRESS": "Search...",
|
||||
"BACK": "Back"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"PENDING": "SCAN NOW",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "Error",
|
||||
"SCANNING": "Scanning",
|
||||
"UNKNOWN": "Unknown"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "We couldn't find any scanning results!",
|
||||
"COLUMN_ID": "Vulnerability",
|
||||
"COLUMN_SEVERITY": "Severity",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_FIXED": "Fixed in version",
|
||||
"COLUMN_LAYER": "Introduced in layer",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed",
|
||||
"TOOLTIPS_TITLE": "This tag has {{totalVulnerability}} vulnerabilities across {{totalPackages}} packages."
|
||||
},
|
||||
"SEVERITY": {
|
||||
"HIGH": "High level",
|
||||
"MEDIUM": "Medium level",
|
||||
"LOW": "Low level",
|
||||
"UNKNOWN": "Unknown",
|
||||
"NONE": "None"
|
||||
},
|
||||
"SINGULAR": "Vulnerability",
|
||||
"PLURAL": "Vulnerabilities"
|
||||
},
|
||||
"PUSH_IMAGE": {
|
||||
"TITLE": "Push Image",
|
||||
"TOOLTIP": "Command references for pushing an image to this project.",
|
||||
"TAG_COMMAND": "Tag an image for this project:",
|
||||
"PUSH_COMMAND": "Push an image to this project:",
|
||||
"COPY_ERROR": "Copy failed, please try to manually copy the command references."
|
||||
},
|
||||
"TAG": {
|
||||
"CREATION_TIME_PREFIX": "Create on",
|
||||
"CREATOR_PREFIX": "by",
|
||||
"IMAGE_DETAILS": "Image Details",
|
||||
"ARCHITECTURE": "Architecture",
|
||||
"OS": "OS",
|
||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||
"IMAGE_VULNERABILITIES": "Image Vulnerabilities"
|
||||
},
|
||||
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later.",
|
||||
"UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue your action.",
|
||||
"FORBIDDEN_ERROR": "You do not have the proper privileges to perform the action.",
|
||||
|
@ -434,6 +434,55 @@
|
||||
"IN_PROGRESS": "Buscar...",
|
||||
"BACK": "Volver"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"PENDING": "SCAN NOW",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "Error",
|
||||
"SCANNING": "Scanning",
|
||||
"UNKNOWN": "Unknown"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "We couldn't find any scanning results!",
|
||||
"COLUMN_ID": "Vulnerability",
|
||||
"COLUMN_SEVERITY": "Severity",
|
||||
"COLUMN_PACKAGE": "Package",
|
||||
"COLUMN_VERSION": "Current version",
|
||||
"COLUMN_FIXED": "Fixed in version",
|
||||
"COLUMN_LAYER": "Introduced in layer",
|
||||
"FOOT_ITEMS": "Items",
|
||||
"FOOT_OF": "of"
|
||||
},
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "Scan completed",
|
||||
"TOOLTIPS_TITLE": "This tag has {{totalVulnerability}} vulnerabilities across {{totalPackages}} packages."
|
||||
},
|
||||
"SEVERITY": {
|
||||
"HIGH": "High level",
|
||||
"MEDIUM": "Medium level",
|
||||
"LOW": "Low level",
|
||||
"UNKNOWN": "Unknown",
|
||||
"NONE": "None"
|
||||
},
|
||||
"SINGULAR": "Vulnerability",
|
||||
"PLURAL": "Vulnerabilities"
|
||||
},
|
||||
"PUSH_IMAGE": {
|
||||
"TITLE": "Push Image",
|
||||
"TOOLTIP": "Command references for pushing an image to this project.",
|
||||
"TAG_COMMAND": "Tag an image for this project:",
|
||||
"PUSH_COMMAND": "Push an image to this project:",
|
||||
"COPY_ERROR": "Copy failed, please try to manually copy the command references."
|
||||
},
|
||||
"TAG": {
|
||||
"CREATION_TIME_PREFIX": "Create on",
|
||||
"CREATOR_PREFIX": "by",
|
||||
"IMAGE_DETAILS": "Image Details",
|
||||
"ARCHITECTURE": "Architecture",
|
||||
"OS": "OS",
|
||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||
"IMAGE_VULNERABILITIES": "Image Vulnerabilities"
|
||||
},
|
||||
"UNKNOWN_ERROR": "Ha ocurrido un error desconocido. Por favor, inténtelo de nuevo más tarde.",
|
||||
"UNAUTHORIZED_ERROR": "La sesión no es válida o ha caducado. Necesita identificarse de nuevo para llevar a cabo esa acción.",
|
||||
"FORBIDDEN_ERROR": "No tienes permisos para llevar a cabo esa acción.",
|
||||
|
@ -436,6 +436,55 @@
|
||||
"IN_PROGRESS": "搜索中...",
|
||||
"BACK": "返回"
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"PENDING": "开始扫描",
|
||||
"QUEUED": "已入队列",
|
||||
"ERROR": "错误",
|
||||
"SCANNING": "扫描中",
|
||||
"UNKNOWN": "未知"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "没有扫描结果!",
|
||||
"COLUMN_ID": "缺陷码",
|
||||
"COLUMN_SEVERITY": "严重度",
|
||||
"COLUMN_PACKAGE": "组件",
|
||||
"COLUMN_VERSION": "当前版本",
|
||||
"COLUMN_FIXED": "修复版本",
|
||||
"COLUMN_LAYER": "引入层",
|
||||
"FOOT_ITEMS": "项目",
|
||||
"FOOT_OF": "总共"
|
||||
},
|
||||
"CHART": {
|
||||
"SCANNING_TIME": "扫描完成",
|
||||
"TOOLTIPS_TITLE": "在此镜像的{{totalPackages}}包中扫描出{{totalVulnerability}}缺陷。"
|
||||
},
|
||||
"SEVERITY": {
|
||||
"HIGH": "严重",
|
||||
"MEDIUM": "中等",
|
||||
"LOW": "一般",
|
||||
"UNKNOWN": "未知",
|
||||
"NONE": "无"
|
||||
},
|
||||
"SINGULAR": "缺陷",
|
||||
"PLURAL": "缺陷"
|
||||
},
|
||||
"PUSH_IMAGE": {
|
||||
"TITLE": "推送镜像",
|
||||
"TOOLTIP": "推送一个镜像到当前项目的参考命令。",
|
||||
"TAG_COMMAND": "在项目中标记镜像:",
|
||||
"PUSH_COMMAND": "推送镜像到当前项目:",
|
||||
"COPY_ERROR": "拷贝失败,请尝试手动拷贝参考命令。"
|
||||
},
|
||||
"TAG": {
|
||||
"CREATION_TIME_PREFIX": "创建时间:",
|
||||
"CREATOR_PREFIX": "创建者:",
|
||||
"IMAGE_DETAILS": "镜像详情",
|
||||
"ARCHITECTURE": "架构",
|
||||
"OS": "操作系统",
|
||||
"SCAN_COMPLETION_TIME": "扫描完成时间",
|
||||
"IMAGE_VULNERABILITIES": "镜像缺陷"
|
||||
},
|
||||
"UNKNOWN_ERROR": "发生未知错误,请稍后再试。",
|
||||
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续。",
|
||||
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限。",
|
||||
|
3
src/vendor/github.com/astaxie/beego/orm/orm.go
generated
vendored
3
src/vendor/github.com/astaxie/beego/orm/orm.go
generated
vendored
@ -137,10 +137,11 @@ func (o *orm) ReadOrCreate(md interface{}, col1 string, cols ...string) (bool, i
|
||||
if err == ErrNoRows {
|
||||
// Create
|
||||
id, err := o.Insert(md)
|
||||
fmt.Printf("id when create: %d", id)
|
||||
return (err == nil), id, err
|
||||
}
|
||||
|
||||
return false, ind.FieldByIndex(mi.fields.pk.fieldIndex).Int(), err
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
// insert model data to database
|
||||
|
7
tests/admiral.sh
Executable file
7
tests/admiral.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# run admiral for unit test
|
||||
name=admiral
|
||||
port=8282
|
||||
docker rm -f $name 2>/dev/null
|
||||
docker run -d -p $port:8282 --name $name vmware/admiral:dev
|
Loading…
Reference in New Issue
Block a user