From 1dffe91f91d37c4d8e62059427d020cc60c58ee4 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 26 May 2017 17:23:01 +0800 Subject: [PATCH 01/14] bug fix --- src/ui/api/utils.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/ui/api/utils.go b/src/ui/api/utils.go index 1a90a479d..1c66e152b 100644 --- a/src/ui/api/utils.go +++ b/src/ui/api/utils.go @@ -386,6 +386,29 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []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) } From 58fc326fb25621233fd98528f9b4d8867c9b6c61 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Fri, 9 Jun 2017 13:07:26 +0800 Subject: [PATCH 02/14] use *ngFor to replace *clrDgItems in user datagird --- src/ui_ng/src/app/user/user.component.html | 8 ++-- src/ui_ng/src/app/user/user.component.ts | 46 +++++++++++++++++++--- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/ui_ng/src/app/user/user.component.html b/src/ui_ng/src/app/user/user.component.html index 6c351efd7..516adf19a 100644 --- a/src/ui_ng/src/app/user/user.component.html +++ b/src/ui_ng/src/app/user/user.component.html @@ -6,18 +6,18 @@ - +
- + {{'USER.COLUMN_NAME' | translate}} {{'USER.COLUMN_ADMIN' | translate}} {{'USER.COLUMN_EMAIL' | translate}} {{'USER.COLUMN_REG_NAME' | translate}} - + @@ -28,7 +28,7 @@ {{user.creation_time | date: 'short'}} {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} users - {{'USER.ITEMS' | translate}} + {{'USER.ITEMS' | translate}} diff --git a/src/ui_ng/src/app/user/user.component.ts b/src/ui_ng/src/app/user/user.component.ts index 3195d9ff6..ff2589755 100644 --- a/src/ui_ng/src/app/user/user.component.ts +++ b/src/ui_ng/src/app/user/user.component.ts @@ -14,6 +14,7 @@ import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; import 'rxjs/add/operator/toPromise'; import { Subscription } from 'rxjs/Subscription'; +import { State } from 'Clarity-Angular'; import { UserService } from './user.service'; import { User } from './user'; @@ -27,6 +28,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 +55,8 @@ export class UserComponent implements OnInit, OnDestroy { private deletionSubscription: Subscription; currentTerm: string; + totalCount: number = 0; + currentPage: number = 1; @ViewChild(NewUserModalComponent) private newUserDialog: NewUserModalComponent; @@ -76,7 +89,7 @@ export class UserComponent implements OnInit, OnDestroy { } private isMatchFilterTerm(terms: string, testedItem: string): boolean { - return testedItem.indexOf(terms) != -1; + return testedItem.toLowerCase().indexOf(terms.toLowerCase()) != -1; } public get canCreateUser(): boolean { @@ -111,7 +124,6 @@ export class UserComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.refreshUser(); } ngOnDestroy(): void { @@ -125,7 +137,7 @@ 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); @@ -203,7 +215,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; @@ -212,7 +224,8 @@ export class UserComponent implements OnInit, OnDestroy { .then(users => { this.onGoing = false; - this.users = users; + this.totalCount = users.length; + this.users = users.slice(from, to);//First page return users; }) .catch(error => { @@ -232,7 +245,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: State): 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); } } From 2583b19bcf1b8ec6ddeb4f76c59a5cef513ad923 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Fri, 9 Jun 2017 13:37:15 +0800 Subject: [PATCH 03/14] remove Clarity-Angular module import --- src/ui_ng/src/app/user/user.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui_ng/src/app/user/user.component.ts b/src/ui_ng/src/app/user/user.component.ts index ff2589755..4c7d7f5ff 100644 --- a/src/ui_ng/src/app/user/user.component.ts +++ b/src/ui_ng/src/app/user/user.component.ts @@ -14,7 +14,6 @@ import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; import 'rxjs/add/operator/toPromise'; import { Subscription } from 'rxjs/Subscription'; -import { State } from 'Clarity-Angular'; import { UserService } from './user.service'; import { User } from './user'; @@ -249,7 +248,7 @@ export class UserComponent implements OnInit, OnDestroy { } //Data loading - load(state: State): void { + load(state: any): void { if (state && state.page) { if(this.originalUsers){ this.originalUsers.then(users => { From 0925fd35f109831a3fad80c81e6e3ba07fef4701 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Tue, 6 Jun 2017 17:51:40 +0800 Subject: [PATCH 04/14] implement project manager based on PMS --- .travis.yml | 1 + src/common/const.go | 3 - src/common/models/project.go | 13 +- src/ui/config/config.go | 9 +- src/ui/filter/security.go | 11 +- src/ui/projectmanager/pms/pm.go | 410 ++++++++++++++++++++++++++ src/ui/projectmanager/pms/pm_test.go | 424 +++++++++++++++++++++++++++ tests/admiral.sh | 7 + 8 files changed, 856 insertions(+), 22 deletions(-) create mode 100644 src/ui/projectmanager/pms/pm.go create mode 100644 src/ui/projectmanager/pms/pm_test.go create mode 100755 tests/admiral.sh diff --git a/.travis.yml b/.travis.yml index 9070921e2..589e0aaab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/src/common/const.go b/src/common/const.go index 0598dc2a3..1c4282946 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -28,9 +28,6 @@ const ( RoleDeveloper = 2 RoleGuest = 3 - DeployModeStandAlone = "standalone" - DeployModeIntegration = "integration" - ExtEndpoint = "ext_endpoint" AUTHMode = "auth_mode" DatabaseType = "database_type" diff --git a/src/common/models/project.go b/src/common/models/project.go index db7f62d5f..883f32ce3 100644 --- a/src/common/models/project.go +++ b/src/common/models/project.go @@ -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 diff --git a/src/ui/config/config.go b/src/ui/config/config.go index 50e5add79..206e3fcc6 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -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 -} diff --git a/src/ui/filter/security.go b/src/ui/filter/security.go index b50803d99..4735f4c33 100644 --- a/src/ui/filter/security.go +++ b/src/ui/filter/security.go @@ -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 diff --git a/src/ui/projectmanager/pms/pm.go b/src/ui/projectmanager/pms/pm.go new file mode 100644 index 000000000..78dd16154 --- /dev/null +++ b/src/ui/projectmanager/pms/pm.go @@ -0,0 +1,410 @@ +// 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" + "github.com/vmware/harbor/src/common/utils/log" + er "github.com/vmware/harbor/src/common/utils/registry/error" +) + +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) { + var key, value interface{} + if id, ok := projectIDOrName.(int64); ok { + key = "customProperties.__harborId" + value = id + } else if name, ok := projectIDOrName.(string); ok { + key = "name" + value = name + } else { + return nil, fmt.Errorf("unsupported type: %v", projectIDOrName) + } + + path := fmt.Sprintf("/projects?$filter=%s eq '%v'", key, value) + data, err := p.send(http.MethodGet, path, nil) + if err != nil { + return nil, err + } + + projects, err := parse(data) + 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 +} + +// 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 ... +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) { + path := "/projects?$filter=isPublic eq 'true'" + data, err := p.send(http.MethodGet, path, nil) + if err != nil { + return nil, err + } + + projects, err := parse(data) + 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 ... +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) + + req.URL.RawQuery = req.URL.Query().Encode() + url := req.URL.String() + + resp, err := p.client.Do(req) + if err != nil { + log.Debugf("\"%s %s\" %d", req.Method, url, 0) + 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 +} diff --git a/src/ui/projectmanager/pms/pm_test.go b/src/ui/projectmanager/pms/pm_test.go new file mode 100644 index 000000000..467fd547b --- /dev/null +++ b/src/ui/projectmanager/pms/pm_test.go @@ -0,0 +1,424 @@ +// 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 pm.Delete(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 pm.Delete(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 pm.Delete(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 pm.Delete(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 pm.Delete(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 pm.Delete(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 pm.Delete(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) { + +} diff --git a/tests/admiral.sh b/tests/admiral.sh new file mode 100755 index 000000000..62269b739 --- /dev/null +++ b/tests/admiral.sh @@ -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 \ No newline at end of file From 2072fc237e1fa8722afe29570130e6499d6746a4 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Mon, 12 Jun 2017 19:40:51 +0800 Subject: [PATCH 05/14] Implement tag detail component & refactor vul summary bar chart --- src/ui_ng/lib/src/service/interface.ts | 23 +- src/ui_ng/lib/src/service/scanning.service.ts | 18 +- src/ui_ng/lib/src/service/tag.service.ts | 25 ++- src/ui_ng/lib/src/tag/index.ts | 6 +- .../lib/src/tag/tag-detail.component.css.ts | 109 ++++++++++ .../lib/src/tag/tag-detail.component.html.ts | 77 +++++++ .../lib/src/tag/tag-detail.component.spec.ts | 118 ++++++++++ src/ui_ng/lib/src/tag/tag-detail.component.ts | 88 ++++++++ src/ui_ng/lib/src/tag/tag.component.spec.ts | 2 +- .../result-bar-chart.component.spec.ts | 120 +++++------ .../result-bar-chart.component.ts | 103 +-------- .../result-grid.component.spec.ts | 8 +- .../result-grid.component.ts | 12 +- .../result-tip.component.spec.ts | 27 ++- .../result-tip.component.ts | 204 +++++++++--------- .../vulnerability-scanning/scanning.css.ts | 17 +- .../vulnerability-scanning/scanning.html.ts | 50 +++-- 17 files changed, 692 insertions(+), 315 deletions(-) create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.css.ts create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.html.ts create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.spec.ts create mode 100644 src/ui_ng/lib/src/tag/tag-detail.component.ts diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index 9e1ac030e..fe8c1e5f4 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -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; } \ No newline at end of file diff --git a/src/ui_ng/lib/src/service/scanning.service.ts b/src/ui_ng/lib/src/service/scanning.service.ts index 38c053dd0..12ae0687c 100644 --- a/src/ui_ng/lib/src/service/scanning.service.ts +++ b/src/ui_ng/lib/src/service/scanning.service.ts @@ -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 | Promise | ScanningResultSummary)} + * @returns {(Observable | Promise | VulnerabilitySummary)} * * @memberOf ScanningResultService */ - abstract getScanningResultSummary(tagId: string): Observable | Promise | ScanningResultSummary; + abstract getVulnerabilityScanningSummary(tagId: string): Observable | Promise | VulnerabilitySummary; /** * Get the detailed vulnerabilities scanning results. * * @abstract * @param {string} tagId - * @returns {(Observable | Promise | ScanningDetailResult[])} + * @returns {(Observable | Promise | VulnerabilityItem[])} * * @memberOf ScanningResultService */ - abstract getScanningResults(tagId: string): Observable | Promise | ScanningDetailResult[]; + abstract getVulnerabilityScanningResults(tagId: string): Observable | Promise | VulnerabilityItem[]; } @Injectable() @@ -47,7 +49,7 @@ export class ScanningResultDefaultService extends ScanningResultService { super(); } - getScanningResultSummary(tagId: string): Observable | Promise | ScanningResultSummary { + getVulnerabilityScanningSummary(tagId: string): Observable | Promise | 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 | Promise | ScanningDetailResult[] { + getVulnerabilityScanningResults(tagId: string): Observable | Promise | VulnerabilityItem[] { if (!tagId || tagId.trim() === '') { return Promise.reject('Bad argument'); } diff --git a/src/ui_ng/lib/src/service/tag.service.ts b/src/ui_ng/lib/src/service/tag.service.ts index dbce14f86..6242bcfc7 100644 --- a/src/ui_ng/lib/src/service/tag.service.ts +++ b/src/ui_ng/lib/src/service/tag.service.ts @@ -52,7 +52,19 @@ export abstract class TagService { * * @memberOf TagService */ - abstract deleteTag(repositoryName: string, tag: string): Observable | Promise | any; + abstract deleteTag(repositoryName: string, tag: string): Observable | Promise | any; + + /** + * Get the specified tag. + * + * @abstract + * @param {string} repositoryName + * @param {string} tag + * @returns {(Observable | Promise | Tag)} + * + * @memberOf TagService + */ + abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable | Promise | 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 | Promise | 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)); + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/index.ts b/src/ui_ng/lib/src/tag/index.ts index 8faf3020b..015404427 100644 --- a/src/ui_ng/lib/src/tag/index.ts +++ b/src/ui_ng/lib/src/tag/index.ts @@ -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[] = [ - TagComponent + TagComponent, + TagDetailComponent ]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.css.ts b/src/ui_ng/lib/src/tag/tag-detail.component.css.ts new file mode 100644 index 000000000..32bf839ba --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.css.ts @@ -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; +} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.html.ts b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts new file mode 100644 index 000000000..21df9f81a --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts @@ -0,0 +1,77 @@ +export const TAG_DETAIL_HTML: string = ` +
+
+
+
+ +
+
+
+ {{tagDetails.name}}:v{{tagDetails.docker_version}} +
+
+ {{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}} +
+
+
+
+
+
+ {{'TAG.IMAGE_DETAILS' | translate }} +
+
+
+
{{'TAG.ARCHITECTURE' | translate }}
+
{{'TAG.OS' | translate }}
+
{{'TAG.SCAN_COMPLETION_TIME' | translate }}
+
+
+
{{tagDetails.architecture}}
+
{{tagDetails.os}}
+
{{scanCompletedDatetime | date}}
+
+
+
+
+
+ {{'TAG.IMAGE_VULNERABILITIES' | translate }} +
+
+
+
+ +
+
+ +
+
+
+
{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}
+
{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}
+
+
+
+ +
+
+ +
+
+
+
{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}
+
{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts new file mode 100644 index 000000000..039415b55 --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts @@ -0,0 +1,118 @@ +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'; + +describe('TagDetailComponent (inline template)', () => { + + let comp: TagDetailComponent; + let fixture: ComponentFixture; + 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 + ], + 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"); + }); + })); + +}); \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.ts b/src/ui_ng/lib/src/tag/tag-detail.component.ts new file mode 100644 index 000000000..b579d8eb9 --- /dev/null +++ b/src/ui_ng/lib/src/tag/tag-detail.component.ts @@ -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 = new EventEmitter(); + + constructor( + private tagService: TagService, + private errorHandler: ErrorHandler) { } + + ngOnInit(): void { + if (this.repositoryId && this.tagId) { + toPromise(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"; + } +} diff --git a/src/ui_ng/lib/src/tag/tag.component.spec.ts b/src/ui_ng/lib/src/tag/tag.component.spec.ts index 6685d1a2d..8f7f7a052 100644 --- a/src/ui_ng/lib/src/tag/tag.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag.component.spec.ts @@ -86,7 +86,7 @@ describe('TagComponent (inline template)', ()=> { fixture.detectChanges(); }); - it('Should load data', async(()=>{ + it('should load data', async(()=>{ expect(spy.calls.any).toBeTruthy(); })); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts index 37d947623..e3fbb19ba 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts @@ -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; 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"); }); })); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts index 34b67c3bc..43bd4fec8 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts @@ -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 = new EventEmitter(); - constructor( - private scanningService: ScanningResultService, - private errorHandler: ErrorHandler) { } - - ngOnInit(): void { - toPromise(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); - } - - } diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts index d0578f70d..1213c62ad 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts @@ -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, RequestQueryParams } from '../service/index'; +import { VulnerabilityItem, VulnerabilitySeverity, RequestQueryParams } from '../service/index'; import { ResultGridComponent } from './result-grid.component'; import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service'; @@ -43,9 +43,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 +57,7 @@ describe('ResultGridComponent (inline template)', () => { mockData.push(res); } - spy = spyOn(scanningService, 'getScanningResults') + spy = spyOn(scanningService, 'getVulnerabilityScanningResults') .and.returnValue(Promise.resolve(mockData)); fixture.detectChanges(); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts index 8356d7eb0..a3d340b62 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts @@ -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,13 +27,13 @@ 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(this.scanningService.getScanningResults(tagId)) - .then((results: ScanningDetailResult[]) => { + toPromise(this.scanningService.getVulnerabilityScanningResults(tagId)) + .then((results: VulnerabilityItem[]) => { this.scanningResults = results; }) .catch(error => { this.errorHandler.error(error) }) diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts index 824037ff6..48114db9a 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts @@ -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"); + }); + })); + }); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts index cf35ae836..f97078ecc 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts @@ -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"; } } diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts index a16c9ba93..c10a11f7a 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts @@ -55,7 +55,7 @@ export const SCANNING_STYLES: string = ` .bar-tooltip-font { font-size: 13px; - color: #565656; + color: #ffffff; } .bar-tooltip-font-title { @@ -63,19 +63,16 @@ export const SCANNING_STYLES: string = ` } .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 ul li { - list-style-type: none; - margin: 2px; +.bar-summary-item { + margin-top: 3px; + margin-bottom: 3px; } `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts index 95fdbfc24..a7ee640d6 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts @@ -1,24 +1,40 @@ export const TIP_COMPONENT_HTML: string = ` -
+
-
+
+
+
+
+
- - - - - {{tipTitle}}
-
- {{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} - {{completeDateTime | date}} -
-
    -
  • {{item.id}} {{item.version}} {{item.package}}
  • -
+
+
+ + {{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}
+
+ + {{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }} +
+
+ + {{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }} +
+
+ + {{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }} +
+
+ + {{noneCount}} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }} +
+
+
+ {{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} + {{summary.complete_timestamp | date}}
@@ -75,11 +91,7 @@ export const BAR_CHART_COMPONENT_HTML: string = `
- - - - - +
From 2b3d912f43b19f8f6364204c1ddc394ff09821f2 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Tue, 13 Jun 2017 13:22:09 +0800 Subject: [PATCH 06/14] update i18n messages --- src/ui_ng/package.json | 3 +- src/ui_ng/src/i18n/lang/en-us-lang.json | 49 +++++++++++++++++++++++++ src/ui_ng/src/i18n/lang/es-es-lang.json | 49 +++++++++++++++++++++++++ src/ui_ng/src/i18n/lang/zh-cn-lang.json | 49 +++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 126283738..92453d5d0 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -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": { diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json index d0f469856..12e5167dc 100644 --- a/src/ui_ng/src/i18n/lang/en-us-lang.json +++ b/src/ui_ng/src/i18n/lang/en-us-lang.json @@ -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.", diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json index d924a54a2..e065d22c3 100644 --- a/src/ui_ng/src/i18n/lang/es-es-lang.json +++ b/src/ui_ng/src/i18n/lang/es-es-lang.json @@ -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.", diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json index 988b50e01..1144383e5 100644 --- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json @@ -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": "当前操作被禁止,请确认你有合法的权限。", From 5a254450968048d111d6932bd7143a9aaf8563a6 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Tue, 13 Jun 2017 15:15:09 +0800 Subject: [PATCH 07/14] update according to the comments --- .../utils/{registry => }/error/error.go | 0 .../utils/{registry => }/error/error_test.go | 0 .../utils/registry/auth/tokenauthorizer.go | 2 +- src/common/utils/registry/registry.go | 2 +- src/common/utils/registry/repository.go | 2 +- src/common/utils/registry/repository_test.go | 2 +- src/ui/api/repository.go | 2 +- src/ui/api/target.go | 2 +- src/ui/api/utils.go | 2 +- src/ui/projectmanager/pms/pm.go | 54 +++++++++++-------- 10 files changed, 40 insertions(+), 28 deletions(-) rename src/common/utils/{registry => }/error/error.go (100%) rename src/common/utils/{registry => }/error/error_test.go (100%) diff --git a/src/common/utils/registry/error/error.go b/src/common/utils/error/error.go similarity index 100% rename from src/common/utils/registry/error/error.go rename to src/common/utils/error/error.go diff --git a/src/common/utils/registry/error/error_test.go b/src/common/utils/error/error_test.go similarity index 100% rename from src/common/utils/registry/error/error_test.go rename to src/common/utils/error/error_test.go diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index d2e8025d8..2ea1f0cb0 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -27,7 +27,7 @@ import ( //"github.com/vmware/harbor/src/common/config" "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" ) diff --git a/src/common/utils/registry/registry.go b/src/common/utils/registry/registry.go index 9fcbc2110..19e5638e5 100644 --- a/src/common/utils/registry/registry.go +++ b/src/common/utils/registry/registry.go @@ -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 diff --git a/src/common/utils/registry/repository.go b/src/common/utils/registry/repository.go index 17df82721..d431657e9 100644 --- a/src/common/utils/registry/repository.go +++ b/src/common/utils/registry/repository.go @@ -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 diff --git a/src/common/utils/registry/repository_test.go b/src/common/utils/registry/repository_test.go index 03ed940aa..22c7fd62d 100644 --- a/src/common/utils/registry/repository_test.go +++ b/src/common/utils/registry/repository_test.go @@ -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" ) diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go index 0782ca04e..0c5aa8244 100644 --- a/src/ui/api/repository.go +++ b/src/ui/api/repository.go @@ -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" ) diff --git a/src/ui/api/target.go b/src/ui/api/target.go index 4984bce3a..216453158 100644 --- a/src/ui/api/target.go +++ b/src/ui/api/target.go @@ -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" ) diff --git a/src/ui/api/utils.go b/src/ui/api/utils.go index a4b4974e0..6351daa32 100644 --- a/src/ui/api/utils.go +++ b/src/ui/api/utils.go @@ -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" ) diff --git a/src/ui/projectmanager/pms/pm.go b/src/ui/projectmanager/pms/pm.go index 78dd16154..d5db15496 100644 --- a/src/ui/projectmanager/pms/pm.go +++ b/src/ui/projectmanager/pms/pm.go @@ -28,8 +28,8 @@ import ( "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" - er "github.com/vmware/harbor/src/common/utils/registry/error" ) var transport = &http.Transport{} @@ -78,24 +78,16 @@ func (p *ProjectManager) Get(projectIDOrName interface{}) (*models.Project, erro } func (p *ProjectManager) get(projectIDOrName interface{}) (*project, error) { - var key, value interface{} + m := map[string]string{} if id, ok := projectIDOrName.(int64); ok { - key = "customProperties.__harborId" - value = id + m["customProperties.__harborId"] = strconv.FormatInt(id, 10) } else if name, ok := projectIDOrName.(string); ok { - key = "name" - value = name + m["name"] = name } else { return nil, fmt.Errorf("unsupported type: %v", projectIDOrName) } - path := fmt.Sprintf("/projects?$filter=%s eq '%v'", key, value) - data, err := p.send(http.MethodGet, path, nil) - if err != nil { - return nil, err - } - - projects, err := parse(data) + projects, err := p.filter(m) if err != nil { return nil, err } @@ -111,6 +103,26 @@ func (p *ProjectManager) get(projectIDOrName interface{}) (*project, error) { 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 { @@ -213,6 +225,7 @@ func (p *ProjectManager) Exist(projectIDOrName interface{}) (bool, error) { } // 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 @@ -276,13 +289,11 @@ func (p *ProjectManager) getIDbyHarborIDOrName(projectIDOrName interface{}) (str // GetPublic ... func (p *ProjectManager) GetPublic() ([]*models.Project, error) { - path := "/projects?$filter=isPublic eq 'true'" - data, err := p.send(http.MethodGet, path, nil) - if err != nil { - return nil, err + m := map[string]string{ + "isPublic": "true", } - projects, err := parse(data) + projects, err := p.filter(m) if err != nil { return nil, err } @@ -369,7 +380,8 @@ func (p *ProjectManager) GetTotal(query *models.ProjectQueryParam) (int64, error return 0, errors.New("get total of projects is unsupported") } -// GetHasReadPerm ... +// 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 @@ -383,12 +395,12 @@ func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, erro req.Header.Add("x-xenon-auth-token", p.token) - req.URL.RawQuery = req.URL.Query().Encode() url := req.URL.String() + req.URL.RawQuery = req.URL.Query().Encode() resp, err := p.client.Do(req) if err != nil { - log.Debugf("\"%s %s\" %d", req.Method, url, 0) + log.Debugf("\"%s %s\" failed", req.Method, url) return nil, err } defer resp.Body.Close() From 71f3480932d2a521c1ddeb1fcd2e2ad03196acc9 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Tue, 13 Jun 2017 16:09:46 +0800 Subject: [PATCH 08/14] Improve repo-tag-stack view --- .../repository-stackview.component.html.ts | 2 +- .../repository-stackview.component.spec.ts | 117 +++++++++--------- .../repository-stackview.component.ts | 51 +++++--- src/ui_ng/lib/src/tag/tag.component.spec.ts | 26 +--- src/ui_ng/lib/src/tag/tag.component.ts | 67 +++++----- 5 files changed, 129 insertions(+), 134 deletions(-) diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts index 01e2402dd..fe0f6a31a 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts @@ -21,7 +21,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = ` {{r.name}} {{r.tags_count}} {{r.pull_count}} - + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts index 8cd205e33..de680f10d 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.spec.ts @@ -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; let repositoryService: RepositoryService; - let spyRepos: jasmine.Spy; - - let compTag: TagComponent; - let fixtureTag: ComponentFixture; 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'); + }); + })); + }); \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts index 41d848753..c6d1a5ce3 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts @@ -5,9 +5,13 @@ 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'; @@ -21,7 +25,7 @@ import { Subscription } from 'rxjs/Subscription'; @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 { @@ -33,20 +37,30 @@ export class RepositoryStackviewComponent implements OnInit { lastFilteredRepoName: string; repositories: Repository[]; + systemInfo: SystemInfo; @ViewChild('confirmationDialog') confirmationDialog: ConfirmationDialogComponent; pullCountComparator: Comparator = new CustomComparator('pull_count', 'number'); - + tagsCountComparator: Comparator = new CustomComparator('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 +69,24 @@ export class RepositoryStackviewComponent implements OnInit { toPromise(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(this.systemInfoService.getSystemInfo()) + .then(systemInfo => this.systemInfo = systemInfo) + .catch(error => this.errorHandler.error(error)); + this.lastFilteredRepoName = ''; this.retrieve(); } @@ -76,10 +95,10 @@ export class RepositoryStackviewComponent implements OnInit { toPromise(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) { diff --git a/src/ui_ng/lib/src/tag/tag.component.spec.ts b/src/ui_ng/lib/src/tag/tag.component.spec.ts index 8f7f7a052..ce5877a85 100644 --- a/src/ui_ng/lib/src/tag/tag.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag.component.spec.ts @@ -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; 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,11 +60,11 @@ 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(); }); diff --git a/src/ui_ng/lib/src/tag/tag.component.ts b/src/ui_ng/lib/src/tag/tag.component.ts index 50e736a5f..361fcfef4 100644 --- a/src/ui_ng/lib/src/tag/tag.component.ts +++ b/src/ui_ng/lib/src/tag/tag.component.ts @@ -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,24 @@ 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(); tags: Tag[]; - registryUrl: string; - withNotary: boolean; showTagManifestOpened: boolean; manifestInfoTitle: string; @@ -71,10 +70,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 +84,28 @@ export class TagComponent implements OnInit { return; } else { toPromise(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(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 +113,20 @@ export class TagComponent implements OnInit { this.tags = []; this.loading = true; toPromise(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,7 +155,7 @@ 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; From 1d543c9212e5e030c116fde4c1006aeba8cacaa9 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Tue, 13 Jun 2017 20:38:21 +0800 Subject: [PATCH 09/14] Improve components in UI library --- .../src/endpoint/endpoint.component.css.ts | 9 +++++-- .../src/endpoint/endpoint.component.html.ts | 12 +++++---- src/ui_ng/lib/src/filter/filter.component.ts | 15 +++++++++++ src/ui_ng/lib/src/filter/filter.template.ts | 26 +++++++++++++++++-- .../list-replication-rule.component.html.ts | 9 ++++--- src/ui_ng/lib/src/log/recent-log.component.ts | 8 +++--- src/ui_ng/lib/src/log/recent-log.template.ts | 7 ++--- .../replication/replication.component.css.ts | 11 ++++++-- .../replication/replication.component.html.ts | 18 ++++++------- .../repository-stackview.component.css.ts | 9 ++++--- .../repository-stackview.component.html.ts | 10 ++++--- .../lib/src/tag/tag-detail.component.spec.ts | 4 ++- src/ui_ng/lib/src/utils.ts | 7 ++++- .../result-grid.component.spec.ts | 3 ++- .../result-grid.component.ts | 8 ++++++ .../vulnerability-scanning/scanning.css.ts | 24 +++++++---------- .../vulnerability-scanning/scanning.html.ts | 12 ++++++++- 17 files changed, 137 insertions(+), 55 deletions(-) diff --git a/src/ui_ng/lib/src/endpoint/endpoint.component.css.ts b/src/ui_ng/lib/src/endpoint/endpoint.component.css.ts index 8eab04166..6452b5d9a 100644 --- a/src/ui_ng/lib/src/endpoint/endpoint.component.css.ts +++ b/src/ui_ng/lib/src/endpoint/endpoint.component.css.ts @@ -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; } `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts b/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts index 926d79083..ebd4ce84d 100644 --- a/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts +++ b/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts @@ -1,17 +1,17 @@ export const ENDPOINT_TEMPLATE: string = ` - +
-
+
- - + + - +
@@ -37,4 +37,6 @@ export const ENDPOINT_TEMPLATE: string = `
+ +
`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/filter/filter.component.ts b/src/ui_ng/lib/src/filter/filter.component.ts index 7e6f90a3c..5f7c80ada 100644 --- a/src/ui_ng/lib/src/filter/filter.component.ts +++ b/src/ui_ng/lib/src/filter/filter.component.ts @@ -31,6 +31,7 @@ export class FilterComponent implements OnInit { placeHolder: string = ""; filterTerms = new Subject(); + isExpanded: boolean = false; @Output("filter") private filterEvt = new EventEmitter(); @@ -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); + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/filter/filter.template.ts b/src/ui_ng/lib/src/filter/filter.template.ts index 1f55ad227..9bed73db5 100644 --- a/src/ui_ng/lib/src/filter/filter.template.ts +++ b/src/ui_ng/lib/src/filter/filter.template.ts @@ -4,8 +4,9 @@ export const FILTER_TEMPLATE: string = ` - - + + + `; @@ -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; +} `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts index 83e5df4eb..3292a7f42 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts @@ -1,6 +1,5 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = ` - - +
{{'REPLICATION.NAME' | translate}} {{'REPLICATION.PROJECT' | translate}} @@ -37,4 +36,8 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = ` {{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'REPLICATION.OF' | translate}} {{pagination.totalItems }} {{'REPLICATION.ITEMS' | translate}} -`; \ No newline at end of file + + + +
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/log/recent-log.component.ts b/src/ui_ng/lib/src/log/recent-log.component.ts index 601f4345f..3e6b411c2 100644 --- a/src/ui_ng/lib/src/log/recent-log.component.ts +++ b/src/ui_ng/lib/src/log/recent-log.component.ts @@ -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 = new CustomComparator('op_time', 'date'); + opTimeComparator: Comparator = new CustomComparator('op_time', 'date'); constructor( private logService: AccessLogService, diff --git a/src/ui_ng/lib/src/log/recent-log.template.ts b/src/ui_ng/lib/src/log/recent-log.template.ts index 95fcf5d1d..9abf1081c 100644 --- a/src/ui_ng/lib/src/log/recent-log.template.ts +++ b/src/ui_ng/lib/src/log/recent-log.template.ts @@ -4,11 +4,11 @@ export const LOG_TEMPLATE: string = `
-

{{'SIDE_NAV.LOGS' | translate}}

+

{{'SIDE_NAV.LOGS' | translate}}

- + @@ -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 { diff --git a/src/ui_ng/lib/src/replication/replication.component.css.ts b/src/ui_ng/lib/src/replication/replication.component.css.ts index 7e5276d15..850dfc259 100644 --- a/src/ui_ng/lib/src/replication/replication.component.css.ts +++ b/src/ui_ng/lib/src/replication/replication.component.css.ts @@ -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 { diff --git a/src/ui_ng/lib/src/replication/replication.component.html.ts b/src/ui_ng/lib/src/replication/replication.component.html.ts index 9c8f4984c..283a45e3c 100644 --- a/src/ui_ng/lib/src/replication/replication.component.html.ts +++ b/src/ui_ng/lib/src/replication/replication.component.html.ts @@ -1,21 +1,21 @@ export const REPLICATION_TEMPLATE: string = `
-
+
-
+
- - + + - +
@@ -23,14 +23,14 @@ export const REPLICATION_TEMPLATE: string = `
-
+
{{'REPLICATION.REPLICATION_JOBS' | translate}}
- - + + - +
diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts index fdbaab321..3a0a4588e 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts @@ -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; diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts index fe0f6a31a..b9d935914 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts @@ -1,11 +1,11 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = ` - +
-
+
- - + +
@@ -31,4 +31,6 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
+ +
`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts index 039415b55..ecdbab517 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts @@ -8,6 +8,7 @@ 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)', () => { @@ -47,7 +48,8 @@ describe('TagDetailComponent (inline template)', () => { ], declarations: [ TagDetailComponent, - ResultGridComponent + ResultGridComponent, + FilterComponent ], providers: [ ErrorHandler, diff --git a/src/ui_ng/lib/src/utils.ts b/src/ui_ng/lib/src/utils.ts index 3c5fb9add..cd7c8eb77 100644 --- a/src/ui_ng/lib/src/utils.ts +++ b/src/ui_ng/lib/src/utils.ts @@ -118,4 +118,9 @@ export class CustomComparator implements Comparator { } return comp; } -} \ No newline at end of file +} + +/** + * The default page size + */ +export const DEFAULT_PAGE_SIZE: number = 15; \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts index 1213c62ad..1ef98afa2 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts @@ -10,6 +10,7 @@ import { ScanningResultService, ScanningResultDefaultService } from '../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 }, diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts index a3d340b62..99e4750f5 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts @@ -38,4 +38,12 @@ export class ResultGridComponent implements OnInit { }) .catch(error => { this.errorHandler.error(error) }) } + + filterVulnerabilities(terms: string): void { + console.log(terms); + } + + refresh(): void { + this.loadResults(this.tagId); + } } diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts index c10a11f7a..5affed773 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts @@ -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,62 +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: #ffffff; } - .bar-tooltip-font-title { font-weight: 600; } - .bar-summary { margin-top: 12px; text-align: left; } - .bar-scanning-time { margin-top: 12px; } - .bar-summary-item { margin-top: 3px; margin-bottom: 3px; } +.option-right { + padding-right: 16px; +} +.refresh-btn { + cursor: pointer; +} +.refresh-btn:hover { + color: #007CBB; +} `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts index a7ee640d6..f65de9497 100644 --- a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts @@ -42,7 +42,16 @@ export const TIP_COMPONENT_HTML: string = ` `; export const GRID_COMPONENT_HTML: string = ` -
+
+
+
+
+ + +
+
+
+
{{'VULNERABILITY.GRID.COLUMN_ID' | translate}} {{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}} @@ -71,6 +80,7 @@ export const GRID_COMPONENT_HTML: string = ` +
`; From f981415b5b64ee7c51444151c5cd5d57ed5b7b58 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Tue, 13 Jun 2017 22:24:38 +0800 Subject: [PATCH 10/14] make tag name clickable in repo-tag-stack view --- src/ui_ng/lib/src/tag/tag.component.html.ts | 2 +- src/ui_ng/lib/src/tag/tag.component.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ui_ng/lib/src/tag/tag.component.html.ts b/src/ui_ng/lib/src/tag/tag.component.html.ts index e6dd182e5..dcf1a4f7d 100644 --- a/src/ui_ng/lib/src/tag/tag.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -27,7 +27,7 @@ export const TAG_TEMPLATE = ` - {{t.name}} + {{t.name}} docker pull {{registryUrl}}/{{repoName}}:{{t.name}} diff --git a/src/ui_ng/lib/src/tag/tag.component.ts b/src/ui_ng/lib/src/tag/tag.component.ts index 361fcfef4..a32e57db8 100644 --- a/src/ui_ng/lib/src/tag/tag.component.ts +++ b/src/ui_ng/lib/src/tag/tag.component.ts @@ -51,6 +51,7 @@ export class TagComponent implements OnInit { @Input() withNotary: boolean; @Output() refreshRepo = new EventEmitter(); + @Output() tagClickEvent = new EventEmitter(); tags: Tag[]; @@ -105,7 +106,7 @@ export class TagComponent implements OnInit { this.errorHandler.error('Repo name cannot be unset.'); return; } - + this.retrieve(); } @@ -161,7 +162,14 @@ export class TagComponent implements OnInit { this.showTagManifestOpened = true; } } + selectAndCopy($event: any) { $event.target.select(); } + + onTagClick(tag: Tag): void { + if (tag) { + this.tagClickEvent.emit(tag); + } + } } \ No newline at end of file From ae2d868fd43575fa6bd25a9a006bd495c180bcc9 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Tue, 13 Jun 2017 21:46:52 +0800 Subject: [PATCH 11/14] handlers for image scan, store results overview in DB --- make/common/db/registry.sql | 14 +++ make/common/db/registry_sqlite.sql | 13 ++ make/common/templates/clair/config.yaml | 2 +- make/docker-compose.clair.yml | 3 + src/common/dao/dao_test.go | 43 +++++++ src/common/dao/scan_job.go | 67 ++++++++++ src/common/models/base.go | 3 +- src/common/models/scan_job.go | 46 +++++++ src/common/models/token.go | 22 ++++ src/common/utils/clair/client.go | 108 ++++++++++++++++ src/common/utils/clair/utils.go | 37 ++++++ src/common/utils/clair/utils_test.go | 35 ++++++ src/common/utils/log/logger.go | 5 + .../utils/registry/auth/tokenauthorizer.go | 7 +- src/jobservice/config/config.go | 5 + src/jobservice/job/job_test.go | 8 +- src/jobservice/job/jobs.go | 4 + src/jobservice/job/statemachine.go | 5 +- src/jobservice/scan/context.go | 15 +-- src/jobservice/scan/handlers.go | 116 +++++++++++++++++- src/jobservice/utils/utils.go | 50 +++++++- src/ui/service/token/authutils.go | 20 ++- src/ui/service/token/creator.go | 5 +- src/ui/service/token/token_test.go | 4 +- .../github.com/astaxie/beego/orm/orm.go | 3 +- 25 files changed, 605 insertions(+), 35 deletions(-) create mode 100644 src/common/models/token.go create mode 100644 src/common/utils/clair/client.go create mode 100644 src/common/utils/clair/utils.go create mode 100644 src/common/utils/clair/utils_test.go diff --git a/make/common/db/registry.sql b/make/common/db/registry.sql index 62d301b6d..2c4a8ab04 100644 --- a/make/common/db/registry.sql +++ b/make/common/db/registry.sql @@ -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, diff --git a/make/common/db/registry_sqlite.sql b/make/common/db/registry_sqlite.sql index d121a9bae..fecc00610 100644 --- a/make/common/db/registry_sqlite.sql +++ b/make/common/db/registry_sqlite.sql @@ -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); diff --git a/make/common/templates/clair/config.yaml b/make/common/templates/clair/config.yaml index b09a0870f..f93129e4a 100644 --- a/make/common/templates/clair/config.yaml +++ b/make/common/templates/clair/config.yaml @@ -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 diff --git a/make/docker-compose.clair.yml b/make/docker-compose.clair.yml index b6e5b46af..c90b4fb9d 100644 --- a/make/docker-compose.clair.yml +++ b/make/docker-compose.clair.yml @@ -8,6 +8,9 @@ services: jobservice: networks: - harbor-clair + registry: + networks: + - harbor-clair postgres: networks: harbor-clair: diff --git a/src/common/dao/dao_test.go b/src/common/dao/dao_test.go index 7114f6a98..175c04eb5 100644 --- a/src/common/dao/dao_test.go +++ b/src/common/dao/dao_test.go @@ -134,6 +134,12 @@ const publicityOn = 1 const publicityOff = 0 func TestMain(m *testing.M) { + orm.Debug = true + f, err := os.Create("/root/jtdbtest.out") + if err != nil { + panic(err) + } + orm.DebugLog = orm.NewLog(f) databases := []string{"mysql", "sqlite"} for _, database := range databases { log.Infof("run test cases for database: %s", database) @@ -1693,3 +1699,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) +} diff --git a/src/common/dao/scan_job.go b/src/common/dao/scan_job.go index 945c5af17..51f9ffb68 100644 --- a/src/common/dao/scan_job.go +++ b/src/common/dao/scan_job.go @@ -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 +} diff --git a/src/common/models/base.go b/src/common/models/base.go index f861399f2..7fec6cb70 100644 --- a/src/common/models/base.go +++ b/src/common/models/base.go @@ -27,5 +27,6 @@ func init() { new(Role), new(AccessLog), new(ScanJob), - new(RepoRecord)) + new(RepoRecord), + new(ImgScanOverview)) } diff --git a/src/common/models/scan_job.go b/src/common/models/scan_job.go index 286429cbc..343ffb233 100644 --- a/src/common/models/scan_job.go +++ b/src/common/models/scan_job.go @@ -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"` +} diff --git a/src/common/models/token.go b/src/common/models/token.go new file mode 100644 index 000000000..f83bec346 --- /dev/null +++ b/src/common/models/token.go @@ -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"` +} diff --git a/src/common/utils/clair/client.go b/src/common/utils/clair/client.go new file mode 100644 index 000000000..59c4c95dd --- /dev/null +++ b/src/common/utils/clair/client.go @@ -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 +} diff --git a/src/common/utils/clair/utils.go b/src/common/utils/clair/utils.go new file mode 100644 index 000000000..fd9a1310b --- /dev/null +++ b/src/common/utils/clair/utils.go @@ -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 + } +} diff --git a/src/common/utils/clair/utils_test.go b/src/common/utils/clair/utils_test.go new file mode 100644 index 000000000..f478c38f8 --- /dev/null +++ b/src/common/utils/clair/utils_test.go @@ -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)) + } +} diff --git a/src/common/utils/log/logger.go b/src/common/utils/log/logger.go index 90e091407..50bd2af1f 100644 --- a/src/common/utils/log/logger.go +++ b/src/common/utils/log/logger.go @@ -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() diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index 2ea1f0cb0..b1766e5e0 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -25,6 +25,7 @@ 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/error" @@ -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 } diff --git a/src/jobservice/config/config.go b/src/jobservice/config/config.go index 1e2877bc6..e362d2451 100644 --- a/src/jobservice/config/config.go +++ b/src/jobservice/config/config.go @@ -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" +} diff --git a/src/jobservice/job/job_test.go b/src/jobservice/job/job_test.go index 03cc0863a..48f7b608a 100644 --- a/src/jobservice/job/job_test.go +++ b/src/jobservice/job/job_test.go @@ -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 } diff --git a/src/jobservice/job/jobs.go b/src/jobservice/job/jobs.go index 4a5eb48c6..87dc8d854 100644 --- a/src/jobservice/job/jobs.go +++ b/src/jobservice/job/jobs.go @@ -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 } diff --git a/src/jobservice/job/statemachine.go b/src/jobservice/job/statemachine.go index 52d6a1f1f..196b732f7 100644 --- a/src/jobservice/job/statemachine.go +++ b/src/jobservice/job/statemachine.go @@ -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}) } diff --git a/src/jobservice/scan/context.go b/src/jobservice/scan/context.go index bbdb1097d..b14fb39c7 100644 --- a/src/jobservice/scan/context.go +++ b/src/jobservice/scan/context.go @@ -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 } diff --git a/src/jobservice/scan/handlers.go b/src/jobservice/scan/handlers.go index e27296f6d..1d4edec82 100644 --- a/src/jobservice/scan/handlers.go +++ b/src/jobservice/scan/handlers.go @@ -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 } diff --git a/src/jobservice/utils/utils.go b/src/jobservice/utils/utils.go index 1702d98c4..c39a0391d 100644 --- a/src/jobservice/utils/utils.go +++ b/src/jobservice/utils/utils.go @@ -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 +} diff --git a/src/ui/service/token/authutils.go b/src/ui/service/token/authutils.go index f92c7ea34..4544818f6 100644 --- a/src/ui/service/token/authutils.go +++ b/src/ui/service/token/authutils.go @@ -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 { diff --git a/src/ui/service/token/creator.go b/src/ui/service/token/creator.go index 64cc59784..359398bb9 100644 --- a/src/ui/service/token/creator.go +++ b/src/ui/service/token/creator.go @@ -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) diff --git a/src/ui/service/token/token_test.go b/src/ui/service/token/token_test.go index bcb933996..a6a6207bc 100644 --- a/src/ui/service/token/token_test.go +++ b/src/ui/service/token/token_test.go @@ -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) diff --git a/src/vendor/github.com/astaxie/beego/orm/orm.go b/src/vendor/github.com/astaxie/beego/orm/orm.go index 0ffb6b869..e389a9930 100644 --- a/src/vendor/github.com/astaxie/beego/orm/orm.go +++ b/src/vendor/github.com/astaxie/beego/orm/orm.go @@ -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 From 23635b69665d44af34fce1899fd136f4e09ff570 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Wed, 14 Jun 2017 00:00:22 +0800 Subject: [PATCH 12/14] add placeholders to the datagird --- src/ui_ng/lib/README.md | 70 +++++++++++++++---- .../src/endpoint/endpoint.component.html.ts | 1 + .../list-replication-rule.component.html.ts | 1 + .../list-repository.component.html.ts | 1 + .../replication/replication.component.html.ts | 1 + .../repository-stackview.component.html.ts | 2 +- .../repository-stackview.component.ts | 17 ++++- src/ui_ng/lib/src/tag/tag.component.html.ts | 1 + 8 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/ui_ng/lib/README.md b/src/ui_ng/lib/README.md index 6fa8c0ec0..b42a7e74c 100644 --- a/src/ui_ng/lib/README.md +++ b/src/ui_ng/lib/README.md @@ -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 - - + ``` * **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. + ``` - + + +... + +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 | Promise | ScanningResultSummary)} + * @returns {(Observable | Promise | VulnerabilitySummary)} * * @memberOf ScanningResultService */ - getScanningResultSummary(tagId: string): Observable | Promise | ScanningResultSummary { - ... - } + getVulnerabilityScanningSummary(tagId: string): Observable | Promise | VulnerabilitySummary{ + ... + } /** * Get the detailed vulnerabilities scanning results. * * @abstract * @param {string} tagId - * @returns {(Observable | Promise | ScanningDetailResult[])} + * @returns {(Observable | Promise | VulnerabilityItem[])} * * @memberOf ScanningResultService */ - getScanningResults(tagId: string): Observable | Promise | ScanningDetailResult[] { - ... - } + getVulnerabilityScanningResults(tagId: string): Observable | Promise | 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 | Promise | SystemInfo { + ... + } +} + +... +HarborLibraryModule.forRoot({ + systemInfoService: { provide: SystemInfoService, useClass: MySystemInfoService } +}) +... + ``` \ No newline at end of file diff --git a/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts b/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts index ebd4ce84d..1a15723a1 100644 --- a/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts +++ b/src/ui_ng/lib/src/endpoint/endpoint.component.html.ts @@ -20,6 +20,7 @@ export const ENDPOINT_TEMPLATE: string = ` {{'DESTINATION.NAME' | translate}} {{'DESTINATION.URL' | translate}} {{'DESTINATION.CREATION_TIME' | translate}} + {{'DESTINATION.PLACEHOLDER' | translate }} diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts index 3292a7f42..f19268eee 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts @@ -7,6 +7,7 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = ` {{'REPLICATION.DESTINATION_NAME' | translate}} {{'REPLICATION.LAST_START_TIME' | translate}} {{'REPLICATION.ACTIVATION' | translate}} + {{'REPLICATION.PLACEHOLDER' | translate }} diff --git a/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts b/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts index 1a5e65037..158fac220 100644 --- a/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts +++ b/src/ui_ng/lib/src/list-repository/list-repository.component.html.ts @@ -3,6 +3,7 @@ export const LIST_REPOSITORY_TEMPLATE = ` {{'REPOSITORY.NAME' | translate}} {{'REPOSITORY.TAGS_COUNT' | translate}} {{'REPOSITORY.PULL_COUNT' | translate}} + {{'REPOSITORY.PLACEHOLDER' | translate }} diff --git a/src/ui_ng/lib/src/replication/replication.component.html.ts b/src/ui_ng/lib/src/replication/replication.component.html.ts index 283a45e3c..5e89a2843 100644 --- a/src/ui_ng/lib/src/replication/replication.component.html.ts +++ b/src/ui_ng/lib/src/replication/replication.component.html.ts @@ -53,6 +53,7 @@ export const REPLICATION_TEMPLATE: string = ` {{'REPLICATION.CREATION_TIME' | translate}} {{'REPLICATION.END_TIME' | translate}} {{'REPLICATION.LOGS' | translate}} + {{'REPLICATION.JOB_PLACEHOLDER' | translate }} {{j.repository}} {{j.status}} diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts index b9d935914..8ab942517 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.html.ts @@ -21,7 +21,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = ` {{r.name}} {{r.tags_count}} {{r.pull_count}} - + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts index c6d1a5ce3..7321a1134 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts @@ -1,4 +1,13 @@ -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'; @@ -21,6 +30,7 @@ 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', @@ -34,6 +44,7 @@ export class RepositoryStackviewComponent implements OnInit { @Input() hasSignedIn: boolean; @Input() hasProjectAdminRole: boolean; + @Output() tagClickEvent = new EventEmitter(); lastFilteredRepoName: string; repositories: Repository[]; @@ -120,4 +131,8 @@ export class RepositoryStackviewComponent implements OnInit { refresh() { this.retrieve(); } + + watchTagClickEvt(tag: Tag): void { + this.tagClickEvent.emit(tag); + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag.component.html.ts b/src/ui_ng/lib/src/tag/tag.component.html.ts index dcf1a4f7d..1fb6ba5f0 100644 --- a/src/ui_ng/lib/src/tag/tag.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -22,6 +22,7 @@ export const TAG_TEMPLATE = ` {{'REPOSITORY.DOCKER_VERSION' | translate}} {{'REPOSITORY.ARCHITECTURE' | translate}} {{'REPOSITORY.OS' | translate}} + {{'TGA.PLACEHOLDER' | translate }} From 8590c8d6bb7e6aaea72ba12ef34901c768bc10fc Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Wed, 14 Jun 2017 16:11:23 +0800 Subject: [PATCH 13/14] remove orm debug setting from test case --- src/common/dao/dao_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/common/dao/dao_test.go b/src/common/dao/dao_test.go index 175c04eb5..0bdcab1bf 100644 --- a/src/common/dao/dao_test.go +++ b/src/common/dao/dao_test.go @@ -134,12 +134,6 @@ const publicityOn = 1 const publicityOff = 0 func TestMain(m *testing.M) { - orm.Debug = true - f, err := os.Create("/root/jtdbtest.out") - if err != nil { - panic(err) - } - orm.DebugLog = orm.NewLog(f) databases := []string{"mysql", "sqlite"} for _, database := range databases { log.Infof("run test cases for database: %s", database) From 4d0ae36d4edd4faae648225a09b00453f3fc2a48 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 14 Jun 2017 17:01:41 +0800 Subject: [PATCH 14/14] update test case --- src/ui/projectmanager/pms/pm_test.go | 42 +++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/ui/projectmanager/pms/pm_test.go b/src/ui/projectmanager/pms/pm_test.go index 467fd547b..6627bc526 100644 --- a/src/ui/projectmanager/pms/pm_test.go +++ b/src/ui/projectmanager/pms/pm_test.go @@ -183,7 +183,11 @@ func TestGet(t *testing.T) { Name: name, }) require.Nil(t, err) - defer pm.Delete(id) + 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{}) @@ -230,7 +234,11 @@ func TestIsPublic(t *testing.T) { Public: 1, }) require.Nil(t, err) - defer pm.Delete(id) + 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) @@ -247,7 +255,11 @@ func TestIsPublic(t *testing.T) { Public: 0, }) require.Nil(t, err) - defer pm.Delete(id) + 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) @@ -277,7 +289,11 @@ func TestExist(t *testing.T) { Name: name, }) require.Nil(t, err) - defer pm.Delete(id) + 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) @@ -306,7 +322,11 @@ func TestGetRoles(t *testing.T) { Name: name, }) require.Nil(t, err) - defer pm.Delete(id) + 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) @@ -328,7 +348,11 @@ func TestGetPublic(t *testing.T) { Public: 1, }) require.Nil(t, err) - defer pm.Delete(id) + defer func(id int64) { + if err := pm.Delete(id); err != nil { + require.Nil(t, err) + } + }(id) projects, err = pm.GetPublic() assert.Nil(t, nil) @@ -362,7 +386,11 @@ func TestCreate(t *testing.T) { AutomaticallyScanImagesOnPush: true, }) require.Nil(t, err) - defer pm.Delete(id) + 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)