Merge remote-tracking branch 'upstream/master' into 170608_project

This commit is contained in:
Wenkai Yin 2017-06-14 20:46:23 +08:00
commit aecfcef51a
82 changed files with 2773 additions and 603 deletions

View File

@ -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

View File

@ -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,

View File

@ -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);

View File

@ -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

View File

@ -8,6 +8,9 @@ services:
jobservice:
networks:
- harbor-clair
registry:
networks:
- harbor-clair
postgres:
networks:
harbor-clair:

View File

@ -28,9 +28,6 @@ const (
RoleDeveloper = 2
RoleGuest = 3
DeployModeStandAlone = "standalone"
DeployModeIntegration = "integration"
ExtEndpoint = "ext_endpoint"
AUTHMode = "auth_mode"
DatabaseType = "database_type"

View File

@ -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)
}

View File

@ -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
}

View File

@ -27,5 +27,6 @@ func init() {
new(Role),
new(AccessLog),
new(ScanJob),
new(RepoRecord))
new(RepoRecord),
new(ImgScanOverview))
}

View File

@ -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

View File

@ -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"`
}

View 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"`
}

View 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
}

View 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
}
}

View 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))
}
}

View File

@ -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()

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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"
)

View File

@ -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"
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View 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
}

View 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) {
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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 }
})
...
```

View File

@ -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;
}
`;

View File

@ -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>
`;

View File

@ -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);
}
}

View File

@ -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;
}
`;

View File

@ -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>
`;

View File

@ -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>

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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>

View File

@ -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;

View File

@ -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>
`;

View File

@ -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');
});
}));
});

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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');
}

View File

@ -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));
}
}

View File

@ -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
];

View 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;
}
`;

View 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>
`;

View 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");
});
}));
});

View 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";
}
}

View File

@ -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>

View File

@ -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();
}));

View File

@ -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);
}
}
}

View File

@ -118,4 +118,9 @@ export class CustomComparator<T> implements Comparator<T> {
}
return comp;
}
}
}
/**
* The default page size
*/
export const DEFAULT_PAGE_SIZE: number = 15;

View File

@ -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");
});
}));

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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");
});
}));
});

View File

@ -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";
}
}

View File

@ -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;
}
`;

View File

@ -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>

View File

@ -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": {

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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.",

View File

@ -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.",

View File

@ -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": "当前操作被禁止,请确认你有合法的权限。",

View File

@ -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
View 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