Merge branch 'master' of https://github.com/goharbor/harbor into immutable-selector

This commit is contained in:
wang yan 2019-09-19 17:55:29 +08:00
commit cab07f71cd
258 changed files with 10709 additions and 7794 deletions

View File

@ -20,7 +20,8 @@ matrix:
- go: 1.12.5
env:
- OFFLINE=true
- node_js: 10.16.2
- language: node_js
node_js: 10.16.2
env:
- UI_UT=true
env:

View File

@ -43,3 +43,11 @@ The following table depicts the various user permission levels in a project.
| Add/Remove labels of helm chart version | | ✓ | ✓ | ✓ |
| See a list of project robots | | | ✓ | ✓ |
| Create/edit/delete project robots | | | | ✓ |
| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ |
| Create/edit/remove CVE whitelist | | | | ✓ |
| Enable/disable webhooks | | ✓ | ✓ | ✓ |
| Create/delete tag retention rules | | ✓ | ✓ | ✓ |
| Enable/disable tag retention rules | | ✓ | ✓ | ✓ |
| See project quotas | ✓ | ✓ | ✓ | ✓ |
| Edit project quotas | | | | |

View File

@ -0,0 +1,34 @@
/*Table for keeping the plug scanner registration*/
CREATE TABLE scanner_registration
(
id SERIAL PRIMARY KEY NOT NULL,
uuid VARCHAR(64) UNIQUE NOT NULL,
url VARCHAR(256) UNIQUE NOT NULL,
name VARCHAR(128) UNIQUE NOT NULL,
description VARCHAR(1024) NULL,
auth VARCHAR(16) NOT NULL,
access_cred VARCHAR(512) NULL,
adapter VARCHAR(128) NOT NULL,
vendor VARCHAR(128) NOT NULL,
version VARCHAR(32) NOT NULL,
disabled BOOLEAN NOT NULL DEFAULT FALSE,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
skip_cert_verify BOOLEAN NOT NULL DEFAULT FALSE,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
/*Table for keeping the scanner report. The report details are stored as JSONB*/
CREATE TABLE scanner_report
(
id SERIAL PRIMARY KEY NOT NULL,
digest VARCHAR(256) NOT NULL,
registration_id VARCHAR(64) NOT NULL,
job_id VARCHAR(32),
status VARCHAR(16) NOT NULL,
status_code INTEGER DEFAULT 0,
report JSON,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(digest, registration_id)
)

View File

@ -6,6 +6,7 @@ COPY ./LICENSE /portal_src
WORKDIR /build_dir
RUN cp -r /portal_src/* /build_dir \
&& ls -la \
&& apt-get update \
@ -14,7 +15,7 @@ RUN cp -r /portal_src/* /build_dir \
&& npm install \
&& npm run build_lib \
&& npm run link_lib \
&& npm run release
&& node --max_old_space_size=8192 'node_modules/@angular/cli/bin/ng' build --prod
FROM photon:2.0

View File

@ -206,6 +206,13 @@ func init() {
beego.Router("/api/internal/switchquota", &InternalAPI{}, "put:SwitchQuota")
beego.Router("/api/internal/syncquota", &InternalAPI{}, "post:SyncQuota")
// Add routes for plugin scanner management
scannerAPI := &ScannerAPI{}
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
beego.Router("/api/scanners/:uid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
// Add routes for project level scanner
beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
// syncRegistry
if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
log.Fatalf("failed to sync repositories from registry: %v", err)

View File

@ -0,0 +1,348 @@
// Copyright Project Harbor Authors
//
// 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 api
import (
"fmt"
"net/http"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/pkg/errors"
)
// ScannerAPI provides the API for managing the plugin scanners
type ScannerAPI struct {
// The base controller to provide common utilities
BaseController
// Controller for the plug scanners
c api.Controller
}
// Prepare sth. for the subsequent actions
func (sa *ScannerAPI) Prepare() {
// Call super prepare method
sa.BaseController.Prepare()
// Check access permissions
if !sa.SecurityCtx.IsAuthenticated() {
sa.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
if !sa.SecurityCtx.IsSysAdmin() {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
// Use the default controller
sa.c = api.DefaultController
}
// Get the specified scanner
func (sa *ScannerAPI) Get() {
if r := sa.get(); r != nil {
// Response to the client
sa.Data["json"] = r
sa.ServeJSON()
}
}
// List all the scanners
func (sa *ScannerAPI) List() {
p, pz, err := sa.GetPaginationParams()
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: list all"))
return
}
query := &q.Query{
PageSize: pz,
PageNumber: p,
}
// Get query key words
kws := make(map[string]string)
properties := []string{"name", "description", "url"}
for _, k := range properties {
kw := sa.GetString(k)
if len(kw) > 0 {
kws[k] = kw
}
}
if len(kws) > 0 {
query.Keywords = kws
}
all, err := sa.c.ListRegistrations(query)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: list all"))
return
}
// Response to the client
sa.Data["json"] = all
sa.ServeJSON()
}
// Create a new scanner
func (sa *ScannerAPI) Create() {
r := &scanner.Registration{}
if err := sa.DecodeJSONReq(r); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
return
}
if err := r.Validate(false); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
return
}
// Explicitly check if conflict
if !sa.checkDuplicated("name", r.Name) ||
!sa.checkDuplicated("url", r.URL) {
return
}
// All newly created should be non default one except the 1st one
r.IsDefault = false
uuid, err := sa.c.CreateRegistration(r)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: create"))
return
}
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, uuid)
sa.Ctx.ResponseWriter.Header().Add("Location", location)
resp := make(map[string]string, 1)
resp["uuid"] = uuid
// Response to the client
sa.Ctx.ResponseWriter.WriteHeader(http.StatusCreated)
sa.Data["json"] = resp
sa.ServeJSON()
}
// Update a scanner
func (sa *ScannerAPI) Update() {
r := sa.get()
if r == nil {
// meet error
return
}
// full dose updated
rr := &scanner.Registration{}
if err := sa.DecodeJSONReq(rr); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
return
}
if err := r.Validate(true); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
return
}
// Name changed?
if r.Name != rr.Name {
if !sa.checkDuplicated("name", rr.Name) {
return
}
}
// URL changed?
if r.URL != rr.URL {
if !sa.checkDuplicated("url", rr.URL) {
return
}
}
getChanges(r, rr)
if err := sa.c.UpdateRegistration(r); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: update"))
return
}
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, r.UUID)
sa.Ctx.ResponseWriter.Header().Add("Location", location)
// Response to the client
sa.Data["json"] = r
sa.ServeJSON()
}
// Delete the scanner
func (sa *ScannerAPI) Delete() {
uid := sa.GetStringFromPath(":uid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return
}
deleted, err := sa.c.DeleteRegistration(uid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: delete"))
return
}
if deleted == nil {
// Not found
sa.SendNotFoundError(errors.Errorf("scanner registration: %s", uid))
return
}
sa.Data["json"] = deleted
sa.ServeJSON()
}
// SetAsDefault sets the given registration as default one
func (sa *ScannerAPI) SetAsDefault() {
uid := sa.GetStringFromPath(":uid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return
}
m := make(map[string]interface{})
if err := sa.DecodeJSONReq(&m); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set as default"))
return
}
if v, ok := m["is_default"]; ok {
if isDefault, y := v.(bool); y && isDefault {
if err := sa.c.SetDefaultRegistration(uid); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set as default"))
}
return
}
}
// Not supported
sa.SendForbiddenError(errors.Errorf("not supported: %#v", m))
}
// GetProjectScanner gets the project level scanner
func (sa *ScannerAPI) GetProjectScanner() {
pid, err := sa.GetInt64FromPath(":pid")
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: get project scanners"))
return
}
r, err := sa.c.GetRegistrationByProject(pid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get project scanners"))
return
}
if r != nil {
sa.Data["json"] = r
} else {
sa.Data["json"] = make(map[string]interface{})
}
sa.ServeJSON()
}
// SetProjectScanner sets the project level scanner
func (sa *ScannerAPI) SetProjectScanner() {
pid, err := sa.GetInt64FromPath(":pid")
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
body := make(map[string]string)
if err := sa.DecodeJSONReq(&body); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
uuid, ok := body["uuid"]
if !ok || len(uuid) == 0 {
sa.SendBadRequestError(errors.New("missing scanner uuid when setting project scanner"))
return
}
if err := sa.c.SetRegistrationByProject(pid, uuid); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
}
// get the specified scanner
func (sa *ScannerAPI) get() *scanner.Registration {
uid := sa.GetStringFromPath(":uid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return nil
}
r, err := sa.c.GetRegistration(uid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get"))
return nil
}
if r == nil {
// NOT found
sa.SendNotFoundError(errors.Errorf("scanner: %s", uid))
return nil
}
return r
}
func (sa *ScannerAPI) checkDuplicated(property, value string) bool {
// Explicitly check if conflict
kw := make(map[string]string)
kw[property] = value
query := &q.Query{
Keywords: kw,
}
l, err := sa.c.ListRegistrations(query)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: check existence"))
return false
}
if len(l) > 0 {
sa.SendConflictError(errors.Errorf("duplicated entries: %s:%s", property, value))
return false
}
return true
}
func getChanges(e *scanner.Registration, eChange *scanner.Registration) {
e.Name = eChange.Name
e.Description = eChange.Description
e.URL = eChange.URL
e.Auth = eChange.Auth
e.AccessCredential = eChange.AccessCredential
e.Disabled = eChange.Disabled
e.SkipCertVerify = eChange.SkipCertVerify
}

View File

@ -0,0 +1,444 @@
// Copyright Project Harbor Authors
//
// 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 api
import (
"fmt"
"net/http"
"testing"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
const (
rootRoute = "/api/scanners"
)
// ScannerAPITestSuite is test suite for testing the scanner API
type ScannerAPITestSuite struct {
suite.Suite
originC api.Controller
mockC *MockScannerAPIController
}
// TestScannerAPI is the entry of ScannerAPITestSuite
func TestScannerAPI(t *testing.T) {
suite.Run(t, new(ScannerAPITestSuite))
}
// SetupSuite prepares testing env
func (suite *ScannerAPITestSuite) SetupTest() {
suite.originC = api.DefaultController
m := &MockScannerAPIController{}
api.DefaultController = m
suite.mockC = m
}
// TearDownTest clears test case env
func (suite *ScannerAPITestSuite) TearDownTest() {
// Restore
api.DefaultController = suite.originC
}
// TestScannerAPICreate tests the post request to create new one
func (suite *ScannerAPITestSuite) TestScannerAPIBase() {
// Including general cases
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 400
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: sysAdmin,
bodyJSON: &scanner.Registration{
URL: "http://a.b.c",
},
},
code: http.StatusBadRequest,
},
}
runCodeCheckingCases(suite.T(), cases...)
}
// TestScannerAPIGet tests api get
func (suite *ScannerAPITestSuite) TestScannerAPIGet() {
res := &scanner.Registration{
ID: 1000,
UUID: "uuid",
Name: "TestScannerAPIGet",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("GetRegistration", "uuid").Return(res, nil)
// Get
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodGet,
credential: sysAdmin,
}, rr)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), rr)
assert.Equal(suite.T(), res.Name, rr.Name)
assert.Equal(suite.T(), res.UUID, rr.UUID)
}
// TestScannerAPICreate tests create.
func (suite *ScannerAPITestSuite) TestScannerAPICreate() {
r := &scanner.Registration{
Name: "TestScannerAPICreate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockQuery(r)
suite.mockC.On("CreateRegistration", r).Return("uuid", nil)
// Create
res := make(map[string]string, 1)
err := handleAndParse(
&testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: sysAdmin,
bodyJSON: r,
}, &res)
require.NoError(suite.T(), err)
require.Condition(suite.T(), func() (success bool) {
success = res["uuid"] == "uuid"
return
})
}
// TestScannerAPIList tests list
func (suite *ScannerAPITestSuite) TestScannerAPIList() {
query := &q.Query{
PageNumber: 1,
PageSize: 500,
}
ll := []*scanner.Registration{
{
ID: 1001,
UUID: "uuid",
Name: "TestScannerAPIList",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}}
suite.mockC.On("ListRegistrations", query).Return(ll, nil)
// List
l := make([]*scanner.Registration, 0)
err := handleAndParse(&testingRequest{
url: rootRoute,
method: http.MethodGet,
credential: sysAdmin,
}, &l)
require.NoError(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) {
success = len(l) > 0 && l[0].Name == ll[0].Name
return
})
}
// TestScannerAPIUpdate tests the update API
func (suite *ScannerAPITestSuite) TestScannerAPIUpdate() {
before := &scanner.Registration{
ID: 1002,
UUID: "uuid",
Name: "TestScannerAPIUpdate_before",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
updated := &scanner.Registration{
ID: 1002,
UUID: "uuid",
Name: "TestScannerAPIUpdate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockQuery(updated)
suite.mockC.On("UpdateRegistration", updated).Return(nil)
suite.mockC.On("GetRegistration", "uuid").Return(before, nil)
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodPut,
credential: sysAdmin,
bodyJSON: updated,
}, rr)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), rr)
assert.Equal(suite.T(), updated.Name, rr.Name)
assert.Equal(suite.T(), updated.UUID, rr.UUID)
}
//
func (suite *ScannerAPITestSuite) TestScannerAPIDelete() {
r := &scanner.Registration{
ID: 1003,
UUID: "uuid",
Name: "TestScannerAPIDelete",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("DeleteRegistration", "uuid").Return(r, nil)
deleted := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodDelete,
credential: sysAdmin,
}, deleted)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), r.UUID, deleted.UUID)
assert.Equal(suite.T(), r.Name, deleted.Name)
}
// TestScannerAPISetDefault tests the set default
func (suite *ScannerAPITestSuite) TestScannerAPISetDefault() {
suite.mockC.On("SetDefaultRegistration", "uuid").Return(nil)
body := make(map[string]interface{}, 1)
body["is_default"] = true
runCodeCheckingCases(suite.T(), &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodPatch,
credential: sysAdmin,
bodyJSON: body,
},
code: http.StatusOK,
})
}
// TestScannerAPIProjectScanner tests the API of getting/setting project level scanner
func (suite *ScannerAPITestSuite) TestScannerAPIProjectScanner() {
suite.mockC.On("SetRegistrationByProject", int64(1), "uuid").Return(nil)
// Set
body := make(map[string]interface{}, 1)
body["uuid"] = "uuid"
runCodeCheckingCases(suite.T(), &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
method: http.MethodPut,
credential: sysAdmin,
bodyJSON: body,
},
code: http.StatusOK,
})
r := &scanner.Registration{
ID: 1004,
UUID: "uuid",
Name: "TestScannerAPIProjectScanner",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("GetRegistrationByProject", int64(1)).Return(r, nil)
// Get
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
method: http.MethodGet,
credential: sysAdmin,
}, rr)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), r.Name, rr.Name)
assert.Equal(suite.T(), r.UUID, rr.UUID)
}
func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
kw := make(map[string]string, 1)
kw["name"] = r.Name
query := &q.Query{
Keywords: kw,
}
emptyL := make([]*scanner.Registration, 0)
suite.mockC.On("ListRegistrations", query).Return(emptyL, nil)
kw2 := make(map[string]string, 1)
kw2["url"] = r.URL
query2 := &q.Query{
Keywords: kw2,
}
suite.mockC.On("ListRegistrations", query2).Return(emptyL, nil)
}
// MockScannerAPIController is mock of scanner API controller
type MockScannerAPIController struct {
mock.Mock
}
// ListRegistrations ...
func (m *MockScannerAPIController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) {
args := m.Called(query)
return args.Get(0).([]*scanner.Registration), args.Error(1)
}
// CreateRegistration ...
func (m *MockScannerAPIController) CreateRegistration(registration *scanner.Registration) (string, error) {
args := m.Called(registration)
return args.String(0), args.Error(1)
}
// GetRegistration ...
func (m *MockScannerAPIController) GetRegistration(registrationUUID string) (*scanner.Registration, error) {
args := m.Called(registrationUUID)
s := args.Get(0)
if s == nil {
return nil, args.Error(1)
}
return s.(*scanner.Registration), args.Error(1)
}
// RegistrationExists ...
func (m *MockScannerAPIController) RegistrationExists(registrationUUID string) bool {
args := m.Called(registrationUUID)
return args.Bool(0)
}
// UpdateRegistration ...
func (m *MockScannerAPIController) UpdateRegistration(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// DeleteRegistration ...
func (m *MockScannerAPIController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) {
args := m.Called(registrationUUID)
s := args.Get(0)
if s == nil {
return nil, args.Error(1)
}
return s.(*scanner.Registration), args.Error(1)
}
// SetDefaultRegistration ...
func (m *MockScannerAPIController) SetDefaultRegistration(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// SetRegistrationByProject ...
func (m *MockScannerAPIController) SetRegistrationByProject(projectID int64, scannerID string) error {
args := m.Called(projectID, scannerID)
return args.Error(0)
}
// GetRegistrationByProject ...
func (m *MockScannerAPIController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) {
args := m.Called(projectID)
s := args.Get(0)
if s == nil {
return nil, args.Error(1)
}
return s.(*scanner.Registration), args.Error(1)
}
// Ping ...
func (m *MockScannerAPIController) Ping(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// Scan ...
func (m *MockScannerAPIController) Scan(artifact *scan.Artifact) error {
args := m.Called(artifact)
return args.Error(0)
}
// GetReport ...
func (m *MockScannerAPIController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
args := m.Called(artifact)
r := args.Get(0)
if r == nil {
return nil, args.Error(1)
}
return r.([]*dscan.Report), args.Error(1)
}
// GetScanLog ...
func (m *MockScannerAPIController) GetScanLog(digest string) ([]byte, error) {
args := m.Called(digest)
l := args.Get(0)
if l == nil {
return nil, args.Error(1)
}
return l.([]byte), args.Error(1)
}

View File

@ -234,6 +234,12 @@ func (p *ProjectAPI) Post() {
// Head ...
func (p *ProjectAPI) Head() {
if !p.SecurityCtx.IsAuthenticated() {
p.SendUnAuthorizedError(errors.New("Unauthorized"))
return
}
name := p.GetString("project_name")
if len(name) == 0 {
p.SendBadRequestError(errors.New("project_name is needed"))

View File

@ -329,13 +329,13 @@ func TestDeleteProject(t *testing.T) {
}
func TestProHead(t *testing.T) {
fmt.Println("\nTest for Project HEAD API")
t.Log("\nTest for Project HEAD API")
assert := assert.New(t)
apiTest := newHarborAPI()
// ----------------------------case 1 : Response Code=200----------------------------//
fmt.Println("case 1: response code:200")
t.Log("case 1: response code:200")
httpStatusCode, err := apiTest.ProjectsHead(*admin, "library")
if err != nil {
t.Error("Error while search project by proName", err.Error())
@ -345,7 +345,7 @@ func TestProHead(t *testing.T) {
}
// ----------------------------case 2 : Response Code=404:Project name does not exist.----------------------------//
fmt.Println("case 2: response code:404,Project name does not exist.")
t.Log("case 2: response code:404,Project name does not exist.")
httpStatusCode, err = apiTest.ProjectsHead(*admin, "libra")
if err != nil {
t.Error("Error while search project by proName", err.Error())
@ -354,6 +354,24 @@ func TestProHead(t *testing.T) {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
t.Log("case 3: response code:401. Project exist with unauthenticated user")
httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "library")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 404")
}
t.Log("case 4: response code:401. Project name does not exist with unauthenticated user")
httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "libra")
if err != nil {
t.Error("Error while search project by proName", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 404")
}
fmt.Printf("\n")
}

View File

@ -25,8 +25,6 @@ import (
"strconv"
"strings"
"github.com/goharbor/harbor/src/core/filter"
"github.com/astaxie/beego"
"github.com/beego/i18n"
"github.com/goharbor/harbor/src/common"
@ -37,6 +35,7 @@ import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/auth"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/filter"
)
const userKey = "user"

View File

@ -192,6 +192,13 @@ func initRouters() {
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
}
// Add routes for plugin scanner management
scannerAPI := &api.ScannerAPI{}
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
beego.Router("/api/scanners/:uid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
// Add routes for project level scanner
beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
// Error pages
beego.ErrorController(&controllers.ErrorController{})

View File

@ -45,6 +45,7 @@ require (
github.com/google/certificate-transparency-go v1.0.21 // indirect
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/google/uuid v1.1.1
github.com/gorilla/handlers v1.3.0
github.com/gorilla/mux v1.6.2
github.com/graph-gophers/dataloader v5.0.0+incompatible

View File

@ -140,6 +140,8 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeq
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=

View File

@ -88,10 +88,10 @@ func (f *fakedTransfer) Transfer(src *model.Resource, dst *model.Resource) error
}
func TestRun(t *testing.T) {
err := transfer.RegisterFactory("res", fakedTransferFactory)
err := transfer.RegisterFactory("art", fakedTransferFactory)
require.Nil(t, err)
params := map[string]interface{}{
"src_resource": `{"type":"res"}`,
"src_resource": `{"type":"art"}`,
"dst_resource": `{}`,
}
rep := &Replication{}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package res
package art
import (
"encoding/base64"

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package res
package art
// Result keeps the action result
type Result struct {

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package res
package art
// Selector is used to filter the inputting list
type Selector interface {

View File

@ -16,7 +16,7 @@ package doublestar
import (
"github.com/bmatcuk/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/art"
)
const (
@ -46,7 +46,7 @@ type selector struct {
}
// Select candidates by regular expressions
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
func (s *selector) Select(artifacts []*art.Candidate) (selected []*art.Candidate, err error) {
value := ""
excludes := false
@ -86,7 +86,7 @@ func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate
}
// New is factory method for doublestar selector
func New(decoration string, pattern string) res.Selector {
func New(decoration string, pattern string) art.Selector {
return &selector{
decoration: decoration,
pattern: pattern,

View File

@ -16,7 +16,7 @@ package doublestar
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -28,7 +28,7 @@ import (
type RegExpSelectorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
artifacts []*art.Candidate
}
// TestRegExpSelector is entrance for RegExpSelectorTestSuite
@ -38,13 +38,13 @@ func TestRegExpSelector(t *testing.T) {
// SetupSuite to do preparation work
func (suite *RegExpSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{
suite.artifacts = []*art.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "latest",
Kind: res.Image,
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
@ -55,7 +55,7 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
Namespace: "retention",
Repository: "redis",
Tag: "4.0",
Kind: res.Image,
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
@ -66,7 +66,7 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
Namespace: "retention",
Repository: "redis",
Tag: "4.1",
Kind: res.Image,
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
@ -235,7 +235,7 @@ func (suite *RegExpSelectorTestSuite) TestNSExcludes() {
}
// Check whether the returned result matched the expected ones (only check repo:tag)
func expect(expected []string, candidates []*res.Candidate) bool {
func expect(expected []string, candidates []*art.Candidate) bool {
hash := make(map[string]bool)
for _, art := range candidates {

View File

@ -17,8 +17,8 @@ package index
import (
"sync"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
"github.com/pkg/errors"
)
@ -49,11 +49,11 @@ type IndexedMeta struct {
// indexedItem defined item kept in the index
type indexedItem struct {
Meta *IndexedMeta
Factory res.SelectorFactory
Factory art.SelectorFactory
}
// Register the selector with the corresponding selector kind and decoration
func Register(kind string, decorations []string, factory res.SelectorFactory) {
func Register(kind string, decorations []string, factory art.SelectorFactory) {
if len(kind) == 0 || factory == nil {
// do nothing
return
@ -69,7 +69,7 @@ func Register(kind string, decorations []string, factory res.SelectorFactory) {
}
// Get selector with the provided kind and decoration
func Get(kind, decoration, pattern string) (res.Selector, error) {
func Get(kind, decoration, pattern string) (art.Selector, error) {
if len(kind) == 0 || len(decoration) == 0 {
return nil, errors.New("empty selector kind or decoration")
}

View File

@ -17,7 +17,7 @@ package label
import (
"strings"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/art"
)
const (
@ -39,7 +39,7 @@ type selector struct {
}
// Select candidates by the labels
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
func (s *selector) Select(artifacts []*art.Candidate) (selected []*art.Candidate, err error) {
for _, art := range artifacts {
if isMatched(s.labels, art.Labels, s.decoration) {
selected = append(selected, art)
@ -50,7 +50,7 @@ func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate
}
// New is factory method for list selector
func New(decoration string, pattern string) res.Selector {
func New(decoration string, pattern string) art.Selector {
labels := make([]string, 0)
if len(pattern) > 0 {
labels = append(labels, strings.Split(pattern, ",")...)

View File

@ -16,7 +16,7 @@ package label
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -28,7 +28,7 @@ import (
type LabelSelectorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
artifacts []*art.Candidate
}
// TestLabelSelector is entrance for LabelSelectorTestSuite
@ -38,13 +38,13 @@ func TestLabelSelector(t *testing.T) {
// SetupSuite to do preparation work
func (suite *LabelSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{
suite.artifacts = []*art.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "1.9",
Kind: res.Image,
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
@ -55,7 +55,7 @@ func (suite *LabelSelectorTestSuite) SetupSuite() {
Namespace: "library",
Repository: "harbor",
Tag: "dev",
Kind: res.Image,
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
@ -131,7 +131,7 @@ func (suite *LabelSelectorTestSuite) TestWithoutNoneExistingLabels() {
}
// Check whether the returned result matched the expected ones (only check repo:tag)
func expect(expected []string, candidates []*res.Candidate) bool {
func expect(expected []string, candidates []*art.Candidate) bool {
hash := make(map[string]bool)
for _, art := range candidates {

25
src/pkg/q/query.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright Project Harbor Authors
//
// 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 q
// Query parameters
type Query struct {
// Page number
PageNumber int64
// Page size
PageSize int64
// List of key words
Keywords map[string]string
}

View File

@ -21,8 +21,8 @@ import (
"github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/jobservice/config"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/clients/core"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
// DefaultClient for the retention
@ -33,30 +33,30 @@ type Client interface {
// Get the tag candidates under the repository
//
// Arguments:
// repo *res.Repository : repository info
// repo *art.Repository : repository info
//
// Returns:
// []*res.Candidate : candidates returned
// []*art.Candidate : candidates returned
// error : common error if any errors occurred
GetCandidates(repo *res.Repository) ([]*res.Candidate, error)
GetCandidates(repo *art.Repository) ([]*art.Candidate, error)
// Delete the given repository
//
// Arguments:
// repo *res.Repository : repository info
// repo *art.Repository : repository info
//
// Returns:
// error : common error if any errors occurred
DeleteRepository(repo *res.Repository) error
DeleteRepository(repo *art.Repository) error
// Delete the specified candidate
//
// Arguments:
// candidate *res.Candidate : the deleting candidate
// candidate *art.Candidate : the deleting candidate
//
// Returns:
// error : common error if any errors occurred
Delete(candidate *res.Candidate) error
Delete(candidate *art.Candidate) error
}
// NewClient new a basic client
@ -88,13 +88,13 @@ type basicClient struct {
}
// GetCandidates gets the tag candidates under the repository
func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candidate, error) {
func (bc *basicClient) GetCandidates(repository *art.Repository) ([]*art.Candidate, error) {
if repository == nil {
return nil, errors.New("repository is nil")
}
candidates := make([]*res.Candidate, 0)
candidates := make([]*art.Candidate, 0)
switch repository.Kind {
case res.Image:
case art.Image:
images, err := bc.coreClient.ListAllImages(repository.Namespace, repository.Name)
if err != nil {
return nil, err
@ -104,8 +104,8 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
for _, label := range image.Labels {
labels = append(labels, label.Name)
}
candidate := &res.Candidate{
Kind: res.Image,
candidate := &art.Candidate{
Kind: art.Image,
Namespace: repository.Namespace,
Repository: repository.Name,
Tag: image.Name,
@ -118,7 +118,7 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
candidates = append(candidates, candidate)
}
/*
case res.Chart:
case art.Chart:
charts, err := bc.coreClient.ListAllCharts(repository.Namespace, repository.Name)
if err != nil {
return nil, err
@ -128,8 +128,8 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
for _, label := range chart.Labels {
labels = append(labels, label.Name)
}
candidate := &res.Candidate{
Kind: res.Chart,
candidate := &art.Candidate{
Kind: art.Chart,
Namespace: repository.Namespace,
Repository: repository.Name,
Tag: chart.Name,
@ -148,15 +148,15 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
}
// DeleteRepository deletes the specified repository
func (bc *basicClient) DeleteRepository(repo *res.Repository) error {
func (bc *basicClient) DeleteRepository(repo *art.Repository) error {
if repo == nil {
return errors.New("repository is nil")
}
switch repo.Kind {
case res.Image:
case art.Image:
return bc.coreClient.DeleteImageRepository(repo.Namespace, repo.Name)
/*
case res.Chart:
case art.Chart:
return bc.coreClient.DeleteChartRepository(repo.Namespace, repo.Name)
*/
default:
@ -165,15 +165,15 @@ func (bc *basicClient) DeleteRepository(repo *res.Repository) error {
}
// Deletes the specified candidate
func (bc *basicClient) Delete(candidate *res.Candidate) error {
func (bc *basicClient) Delete(candidate *art.Candidate) error {
if candidate == nil {
return errors.New("candidate is nil")
}
switch candidate.Kind {
case res.Image:
case art.Image:
return bc.coreClient.DeleteImage(candidate.Namespace, candidate.Repository, candidate.Tag)
/*
case res.Chart:
case art.Chart:
return bc.coreClient.DeleteChart(candidate.Namespace, candidate.Repository, candidate.Tag)
*/
default:

View File

@ -21,7 +21,7 @@ import (
jmodels "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/testing/clients"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -73,33 +73,33 @@ type clientTestSuite struct {
func (c *clientTestSuite) TestGetCandidates() {
client := &basicClient{}
client.coreClient = &fakeCoreClient{}
var repository *res.Repository
var repository *art.Repository
// nil repository
candidates, err := client.GetCandidates(repository)
require.NotNil(c.T(), err)
// image repository
repository = &res.Repository{}
repository.Kind = res.Image
repository = &art.Repository{}
repository.Kind = art.Image
repository.Namespace = "library"
repository.Name = "hello-world"
candidates, err = client.GetCandidates(repository)
require.Nil(c.T(), err)
assert.Equal(c.T(), 1, len(candidates))
assert.Equal(c.T(), res.Image, candidates[0].Kind)
assert.Equal(c.T(), art.Image, candidates[0].Kind)
assert.Equal(c.T(), "library", candidates[0].Namespace)
assert.Equal(c.T(), "hello-world", candidates[0].Repository)
assert.Equal(c.T(), "latest", candidates[0].Tag)
/*
// chart repository
repository.Kind = res.Chart
repository.Kind = art.Chart
repository.Namespace = "goharbor"
repository.Name = "harbor"
candidates, err = client.GetCandidates(repository)
require.Nil(c.T(), err)
assert.Equal(c.T(), 1, len(candidates))
assert.Equal(c.T(), res.Chart, candidates[0].Kind)
assert.Equal(c.T(), art.Chart, candidates[0].Kind)
assert.Equal(c.T(), "goharbor", candidates[0].Namespace)
assert.Equal(c.T(), "1.0", candidates[0].Tag)
*/
@ -109,20 +109,20 @@ func (c *clientTestSuite) TestDelete() {
client := &basicClient{}
client.coreClient = &fakeCoreClient{}
var candidate *res.Candidate
var candidate *art.Candidate
// nil candidate
err := client.Delete(candidate)
require.NotNil(c.T(), err)
// image
candidate = &res.Candidate{}
candidate.Kind = res.Image
candidate = &art.Candidate{}
candidate.Kind = art.Image
err = client.Delete(candidate)
require.Nil(c.T(), err)
/*
// chart
candidate.Kind = res.Chart
candidate.Kind = art.Chart
err = client.Delete(candidate)
require.Nil(c.T(), err)
*/

View File

@ -23,10 +23,10 @@ import (
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
)
@ -116,7 +116,7 @@ func (pj *Job) Run(ctx job.Context, params job.Parameters) error {
return saveRetainNum(ctx, results, allCandidates)
}
func saveRetainNum(ctx job.Context, retained []*res.Result, allCandidates []*res.Candidate) error {
func saveRetainNum(ctx job.Context, retained []*art.Result, allCandidates []*art.Candidate) error {
var delNum int
for _, r := range retained {
if r.Error == nil {
@ -138,7 +138,7 @@ func saveRetainNum(ctx job.Context, retained []*res.Result, allCandidates []*res
return nil
}
func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Result) {
func logResults(logger logger.Interface, all []*art.Candidate, results []*art.Result) {
hash := make(map[string]error, len(results))
for _, r := range results {
if r.Target != nil {
@ -146,7 +146,7 @@ func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Re
}
}
op := func(art *res.Candidate) string {
op := func(art *art.Candidate) string {
if e, exists := hash[art.Hash()]; exists {
if e != nil {
return actionMarkError
@ -194,7 +194,7 @@ func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Re
}
}
func arn(art *res.Candidate) string {
func arn(art *art.Candidate) string {
return fmt.Sprintf("%s/%s:%s", art.Namespace, art.Repository, art.Tag)
}
@ -237,7 +237,7 @@ func getParamDryRun(params job.Parameters) (bool, error) {
return dryRun, nil
}
func getParamRepo(params job.Parameters) (*res.Repository, error) {
func getParamRepo(params job.Parameters) (*art.Repository, error) {
v, ok := params[ParamRepo]
if !ok {
return nil, errors.Errorf("missing parameter: %s", ParamRepo)
@ -248,7 +248,7 @@ func getParamRepo(params job.Parameters) (*res.Repository, error) {
return nil, errors.Errorf("invalid parameter: %s", ParamRepo)
}
repo := &res.Repository{}
repo := &art.Repository{}
if err := repo.FromJSON(repoJSON); err != nil {
return nil, errors.Wrap(err, "parse repository from JSON")
}

View File

@ -22,14 +22,14 @@ import (
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestps"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -60,10 +60,10 @@ func (suite *JobTestSuite) TearDownSuite() {
func (suite *JobTestSuite) TestRunSuccess() {
params := make(job.Parameters)
params[ParamDryRun] = false
repository := &res.Repository{
repository := &art.Repository{
Namespace: "library",
Name: "harbor",
Kind: res.Image,
Kind: art.Image,
}
repoJSON, err := repository.ToJSON()
require.Nil(suite.T(), err)
@ -112,8 +112,8 @@ func (suite *JobTestSuite) TestRunSuccess() {
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return []*res.Candidate{
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
return []*art.Candidate{
{
Namespace: "library",
Repository: "harbor",
@ -140,12 +140,12 @@ func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Cand
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
return nil
}

View File

@ -19,7 +19,7 @@ import (
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/index"
"github.com/goharbor/harbor/src/pkg/art/selectors/index"
cjob "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
@ -27,12 +27,12 @@ import (
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/q"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
)
@ -84,7 +84,7 @@ func NewLauncher(projectMgr project.Manager, repositoryMgr repository.Manager,
type jobData struct {
TaskID int64
Repository res.Repository
Repository art.Repository
JobName string
JobParams map[string]interface{}
}
@ -111,9 +111,9 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
if scope == nil {
return 0, launcherError(fmt.Errorf("the scope of policy is nil"))
}
repositoryRules := make(map[res.Repository]*lwp.Metadata, 0)
repositoryRules := make(map[art.Repository]*lwp.Metadata, 0)
level := scope.Level
var allProjects []*res.Candidate
var allProjects []*art.Candidate
var err error
if level == "system" {
// get projects
@ -144,12 +144,12 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
}
}
case "project":
projectCandidates = append(projectCandidates, &res.Candidate{
projectCandidates = append(projectCandidates, &art.Candidate{
NamespaceID: scope.Reference,
})
}
var repositoryCandidates []*res.Candidate
var repositoryCandidates []*art.Candidate
// get repositories of projects
for _, projectCandidate := range projectCandidates {
repositories, err := getRepositories(l.projectMgr, l.repositoryMgr, projectCandidate.NamespaceID, l.chartServerEnabled)
@ -174,7 +174,7 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
}
for _, repositoryCandidate := range repositoryCandidates {
reposit := res.Repository{
reposit := art.Repository{
Namespace: repositoryCandidate.Namespace,
Name: repositoryCandidate.Repository,
Kind: repositoryCandidate.Kind,
@ -214,7 +214,7 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
return int64(len(jobDatas)), nil
}
func createJobs(repositoryRules map[res.Repository]*lwp.Metadata, isDryRun bool) ([]*jobData, error) {
func createJobs(repositoryRules map[art.Repository]*lwp.Metadata, isDryRun bool) ([]*jobData, error) {
jobDatas := []*jobData{}
for repository, policy := range repositoryRules {
jobData := &jobData{
@ -320,14 +320,14 @@ func launcherError(err error) error {
return errors.Wrap(err, "launcher")
}
func getProjects(projectMgr project.Manager) ([]*res.Candidate, error) {
func getProjects(projectMgr project.Manager) ([]*art.Candidate, error) {
projects, err := projectMgr.List()
if err != nil {
return nil, err
}
var candidates []*res.Candidate
var candidates []*art.Candidate
for _, pro := range projects {
candidates = append(candidates, &res.Candidate{
candidates = append(candidates, &art.Candidate{
NamespaceID: pro.ProjectID,
Namespace: pro.Name,
})
@ -336,8 +336,8 @@ func getProjects(projectMgr project.Manager) ([]*res.Candidate, error) {
}
func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manager,
projectID int64, chartServerEnabled bool) ([]*res.Candidate, error) {
var candidates []*res.Candidate
projectID int64, chartServerEnabled bool) ([]*art.Candidate, error) {
var candidates []*art.Candidate
/*
pro, err := projectMgr.Get(projectID)
if err != nil {
@ -351,7 +351,7 @@ func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manage
}
for _, r := range imageRepositories {
namespace, repo := utils.ParseRepository(r.Name)
candidates = append(candidates, &res.Candidate{
candidates = append(candidates, &art.Candidate{
Namespace: namespace,
Repository: repo,
Kind: "image",
@ -366,7 +366,7 @@ func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manage
return nil, err
}
for _, r := range chartRepositories {
candidates = append(candidates, &res.Candidate{
candidates = append(candidates, &art.Candidate{
Namespace: pro.Name,
Repository: r.Name,
Kind: "chart",

View File

@ -21,12 +21,12 @@ import (
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/models"
_ "github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/q"
_ "github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
hjob "github.com/goharbor/harbor/src/testing/job"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@ -18,8 +18,8 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -29,7 +29,7 @@ import (
type IndexTestSuite struct {
suite.Suite
candidates []*res.Candidate
candidates []*art.Candidate
}
// TestIndexEntry is entry of IndexTestSuite
@ -41,7 +41,7 @@ func TestIndexEntry(t *testing.T) {
func (suite *IndexTestSuite) SetupSuite() {
Register("fakeAction", newFakePerformer)
suite.candidates = []*res.Candidate{{
suite.candidates = []*art.Candidate{{
Namespace: "library",
Repository: "harbor",
Kind: "image",
@ -77,9 +77,9 @@ type fakePerformer struct {
}
// Perform the artifacts
func (p *fakePerformer) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
func (p *fakePerformer) Perform(candidates []*art.Candidate) (results []*art.Result, err error) {
for _, c := range candidates {
results = append(results, &res.Result{
results = append(results, &art.Result{
Target: c,
})
}

View File

@ -15,8 +15,8 @@
package action
import (
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -29,12 +29,12 @@ type Performer interface {
// Perform the action
//
// Arguments:
// candidates []*res.Candidate : the targets to perform
// candidates []*art.Candidate : the targets to perform
//
// Returns:
// []*res.Result : result infos
// []*art.Result : result infos
// error : common error if any errors occurred
Perform(candidates []*res.Candidate) ([]*res.Result, error)
Perform(candidates []*art.Candidate) ([]*art.Result, error)
}
// PerformerFactory is factory method for creating Performer
@ -42,13 +42,13 @@ type PerformerFactory func(params interface{}, isDryRun bool) Performer
// retainAction make sure all the candidates will be retained and others will be cleared
type retainAction struct {
all []*res.Candidate
all []*art.Candidate
// Indicate if it is a dry run
isDryRun bool
}
// Perform the action
func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
func (ra *retainAction) Perform(candidates []*art.Candidate) (results []*art.Result, err error) {
retained := make(map[string]bool)
for _, c := range candidates {
retained[c.Hash()] = true
@ -56,14 +56,14 @@ func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Res
// start to delete
if len(ra.all) > 0 {
for _, art := range ra.all {
if _, ok := retained[art.Hash()]; !ok {
result := &res.Result{
Target: art,
for _, c := range ra.all {
if _, ok := retained[c.Hash()]; !ok {
result := &art.Result{
Target: c,
}
if !ra.isDryRun {
if err := dep.DefaultClient.Delete(art); err != nil {
if err := dep.DefaultClient.Delete(c); err != nil {
result.Error = err
}
}
@ -79,7 +79,7 @@ func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Res
// NewRetainAction is factory method for RetainAction
func NewRetainAction(params interface{}, isDryRun bool) Performer {
if params != nil {
if all, ok := params.([]*res.Candidate); ok {
if all, ok := params.([]*art.Candidate); ok {
return &retainAction{
all: all,
isDryRun: isDryRun,
@ -88,7 +88,7 @@ func NewRetainAction(params interface{}, isDryRun bool) Performer {
}
return &retainAction{
all: make([]*res.Candidate, 0),
all: make([]*art.Candidate, 0),
isDryRun: isDryRun,
}
}

View File

@ -18,8 +18,8 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -31,7 +31,7 @@ type TestPerformerSuite struct {
suite.Suite
oldClient dep.Client
all []*res.Candidate
all []*art.Candidate
}
// TestPerformer is the entry of the TestPerformerSuite
@ -41,7 +41,7 @@ func TestPerformer(t *testing.T) {
// SetupSuite ...
func (suite *TestPerformerSuite) SetupSuite() {
suite.all = []*res.Candidate{
suite.all = []*art.Candidate{
{
Namespace: "library",
Repository: "harbor",
@ -77,7 +77,7 @@ func (suite *TestPerformerSuite) TestPerform() {
all: suite.all,
}
candidates := []*res.Candidate{
candidates := []*art.Candidate{
{
Namespace: "library",
Repository: "harbor",
@ -100,16 +100,16 @@ func (suite *TestPerformerSuite) TestPerform() {
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
return nil
}
// DeleteRepository ...
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
panic("implement me")
}

View File

@ -18,10 +18,10 @@ import (
"sync"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
)
@ -29,7 +29,7 @@ import (
type processor struct {
// keep evaluator and its related selector if existing
// attentions here, the selectors can be empty/nil, that means match all "**"
evaluators map[*rule.Evaluator][]res.Selector
evaluators map[*rule.Evaluator][]art.Selector
// action performer
performers map[string]action.Performer
}
@ -37,7 +37,7 @@ type processor struct {
// New processor
func New(parameters []*alg.Parameter) alg.Processor {
p := &processor{
evaluators: make(map[*rule.Evaluator][]res.Selector),
evaluators: make(map[*rule.Evaluator][]art.Selector),
performers: make(map[string]action.Performer),
}
@ -59,10 +59,10 @@ func New(parameters []*alg.Parameter) alg.Processor {
}
// Process the candidates with the rules
func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
func (p *processor) Process(artifacts []*art.Candidate) ([]*art.Result, error) {
if len(artifacts) == 0 {
log.Debug("no artifacts to retention")
return make([]*res.Result, 0), nil
return make([]*art.Result, 0), nil
}
var (
@ -75,7 +75,7 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
// for sync
type chanItem struct {
action string
processed []*res.Candidate
processed []*art.Candidate
}
resChan := make(chan *chanItem, 1)
@ -124,9 +124,9 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
for eva, selectors := range p.evaluators {
var evaluator = *eva
go func(evaluator rule.Evaluator, selectors []res.Selector) {
go func(evaluator rule.Evaluator, selectors []art.Selector) {
var (
processed []*res.Candidate
processed []*art.Candidate
err error
)
@ -173,7 +173,7 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
return nil, err
}
results := make([]*res.Result, 0)
results := make([]*art.Result, 0)
// Perform actions
for act, hash := range processedCandidates {
var attachedErr error
@ -192,7 +192,7 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
if attachedErr != nil {
for _, c := range cl {
results = append(results, &res.Result{
results = append(results, &art.Result{
Target: c,
Error: attachedErr,
})
@ -203,10 +203,10 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
return results, nil
}
type cHash map[string]*res.Candidate
type cHash map[string]*art.Candidate
func (ch cHash) toList() []*res.Candidate {
l := make([]*res.Candidate, 0)
func (ch cHash) toList() []*art.Candidate {
l := make([]*art.Candidate, 0)
for _, v := range ch {
l = append(l, v)

View File

@ -19,6 +19,9 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/art/selectors/label"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
@ -26,9 +29,6 @@ import (
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/always"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/lastx"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestps"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -38,7 +38,7 @@ import (
type ProcessorTestSuite struct {
suite.Suite
all []*res.Candidate
all []*art.Candidate
oldClient dep.Client
}
@ -50,7 +50,7 @@ func TestProcessor(t *testing.T) {
// SetupSuite ...
func (suite *ProcessorTestSuite) SetupSuite() {
suite.all = []*res.Candidate{
suite.all = []*art.Candidate{
{
Namespace: "library",
Repository: "harbor",
@ -90,7 +90,7 @@ func (suite *ProcessorTestSuite) TestProcess() {
lastxParams[lastx.ParameterX] = 10
params = append(params, &alg.Parameter{
Evaluator: lastx.New(lastxParams),
Selectors: []res.Selector{
Selectors: []art.Selector{
doublestar.New(doublestar.Matches, "*dev*"),
label.New(label.With, "L1,L2"),
},
@ -101,7 +101,7 @@ func (suite *ProcessorTestSuite) TestProcess() {
latestKParams[latestps.ParameterK] = 10
params = append(params, &alg.Parameter{
Evaluator: latestps.New(latestKParams),
Selectors: []res.Selector{
Selectors: []art.Selector{
label.New(label.With, "L3"),
},
Performer: perf,
@ -131,7 +131,7 @@ func (suite *ProcessorTestSuite) TestProcess2() {
alwaysParams := make(map[string]rule.Parameter)
params = append(params, &alg.Parameter{
Evaluator: always.New(alwaysParams),
Selectors: []res.Selector{
Selectors: []art.Selector{
doublestar.New(doublestar.Matches, "latest"),
label.New(label.With, ""),
},
@ -163,16 +163,16 @@ func (suite *ProcessorTestSuite) TestProcess2() {
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
return nil
}
// DeleteRepository ...
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
panic("implement me")
}

View File

@ -15,9 +15,9 @@
package alg
import (
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
// Processor processing the whole policy targeting a repository.
@ -27,12 +27,12 @@ type Processor interface {
// Process the artifact candidates
//
// Arguments:
// artifacts []*res.Candidate : process the retention candidates
// artifacts []*art.Candidate : process the retention candidates
//
// Returns:
// []*res.Result : the processed results
// []*art.Result : the processed results
// error : common error object if any errors occurred
Process(artifacts []*res.Candidate) ([]*res.Result, error)
Process(artifacts []*art.Candidate) ([]*art.Result, error)
}
// Parameter for constructing a processor
@ -42,7 +42,7 @@ type Parameter struct {
Evaluator rule.Evaluator
// Selectors for the rule
Selectors []res.Selector
Selectors []art.Selector
// Performer for the rule evaluator
Performer action.Performer

View File

@ -21,13 +21,13 @@ import (
index3 "github.com/goharbor/harbor/src/pkg/retention/policy/alg/index"
index2 "github.com/goharbor/harbor/src/pkg/retention/res/selectors/index"
index2 "github.com/goharbor/harbor/src/pkg/art/selectors/index"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/index"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
)
@ -46,7 +46,7 @@ type Builder interface {
}
// NewBuilder news a basic builder
func NewBuilder(all []*res.Candidate) Builder {
func NewBuilder(all []*art.Candidate) Builder {
return &basicBuilder{
allCandidates: all,
}
@ -54,7 +54,7 @@ func NewBuilder(all []*res.Candidate) Builder {
// basicBuilder is default implementation of Builder interface
type basicBuilder struct {
allCandidates []*res.Candidate
allCandidates []*art.Candidate
}
// Build policy processor from the raw policy
@ -76,7 +76,7 @@ func (bb *basicBuilder) Build(policy *lwp.Metadata, isDryRun bool) (alg.Processo
return nil, errors.Wrap(err, "get action performer by metadata")
}
sl := make([]res.Selector, 0)
sl := make([]art.Selector, 0)
for _, s := range r.TagSelectors {
sel, err := index2.Get(s.Kind, s.Decoration, s.Pattern)
if err != nil {

View File

@ -22,7 +22,7 @@ import (
index2 "github.com/goharbor/harbor/src/pkg/retention/policy/alg/index"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/index"
"github.com/goharbor/harbor/src/pkg/art/selectors/index"
"github.com/goharbor/harbor/src/pkg/retention/dep"
@ -30,9 +30,9 @@ import (
"github.com/goharbor/harbor/src/pkg/retention/policy/alg/or"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/goharbor/harbor/src/pkg/art/selectors/label"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestps"
@ -46,7 +46,7 @@ import (
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/stretchr/testify/suite"
)
@ -55,7 +55,7 @@ import (
type TestBuilderSuite struct {
suite.Suite
all []*res.Candidate
all []*art.Candidate
oldClient dep.Client
}
@ -66,7 +66,7 @@ func TestBuilder(t *testing.T) {
// SetupSuite prepares the testing content if needed
func (suite *TestBuilderSuite) SetupSuite() {
suite.all = []*res.Candidate{
suite.all = []*art.Candidate{
{
NamespaceID: 1,
Namespace: "library",
@ -163,21 +163,21 @@ func (suite *TestBuilderSuite) TestBuild() {
type fakeRetentionClient struct{}
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
panic("implement me")
}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *art.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -15,9 +15,9 @@
package always
import (
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -28,7 +28,7 @@ const (
type evaluator struct{}
// Process for the "always" Evaluator simply returns the input with no error
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
return artifacts, nil
}

View File

@ -17,8 +17,8 @@ package always
import (
"testing"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -36,7 +36,7 @@ func (e *EvaluatorTestSuite) TestNew() {
func (e *EvaluatorTestSuite) TestProcess() {
sut := New(rule.Parameters{})
input := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
input := []*art.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
result, err := sut.Process(input)

View File

@ -20,9 +20,9 @@ import (
"time"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -41,7 +41,7 @@ type evaluator struct {
n int
}
func (e *evaluator) Process(artifacts []*res.Candidate) (result []*res.Candidate, err error) {
func (e *evaluator) Process(artifacts []*art.Candidate) (result []*art.Candidate, err error) {
minPullTime := time.Now().UTC().Add(time.Duration(-1*24*e.n) * time.Hour).Unix()
for _, a := range artifacts {
if a.PulledTime >= minPullTime {

View File

@ -20,8 +20,8 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -54,7 +54,7 @@ func (e *EvaluatorTestSuite) TestNew() {
func (e *EvaluatorTestSuite) TestProcess() {
now := time.Now().UTC()
data := []*res.Candidate{
data := []*art.Candidate{
{PulledTime: daysAgo(now, 1)},
{PulledTime: daysAgo(now, 2)},
{PulledTime: daysAgo(now, 3)},

View File

@ -20,9 +20,9 @@ import (
"time"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -41,7 +41,7 @@ type evaluator struct {
n int
}
func (e *evaluator) Process(artifacts []*res.Candidate) (result []*res.Candidate, err error) {
func (e *evaluator) Process(artifacts []*art.Candidate) (result []*art.Candidate, err error) {
minPushTime := time.Now().UTC().Add(time.Duration(-1*24*e.n) * time.Hour).Unix()
for _, a := range artifacts {
if a.PushedTime >= minPushTime {

View File

@ -20,8 +20,8 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -54,7 +54,7 @@ func (e *EvaluatorTestSuite) TestNew() {
func (e *EvaluatorTestSuite) TestProcess() {
now := time.Now().UTC()
data := []*res.Candidate{
data := []*art.Candidate{
{PushedTime: daysAgo(now, 1)},
{PushedTime: daysAgo(now, 2)},
{PushedTime: daysAgo(now, 3)},

View File

@ -14,19 +14,19 @@
package rule
import "github.com/goharbor/harbor/src/pkg/retention/res"
import "github.com/goharbor/harbor/src/pkg/art"
// Evaluator defines method of executing rule
type Evaluator interface {
// Filter the inputs and return the filtered outputs
//
// Arguments:
// artifacts []*res.Candidate : candidates for processing
// artifacts []*art.Candidate : candidates for processing
//
// Returns:
// []*res.Candidate : matched candidates for next stage
// []*art.Candidate : matched candidates for next stage
// error : common error object if any errors occurred
Process(artifacts []*res.Candidate) ([]*res.Candidate, error)
Process(artifacts []*art.Candidate) ([]*art.Candidate, error)
// Specify what action is performed to the candidates processed by this evaluator
Action() string

View File

@ -22,8 +22,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
@ -63,7 +63,7 @@ func (suite *IndexTestSuite) TestGet() {
require.NoError(suite.T(), err)
require.NotNil(suite.T(), evaluator)
candidates := []*res.Candidate{{
candidates := []*art.Candidate{{
Namespace: "library",
Repository: "harbor",
Kind: "image",
@ -102,7 +102,7 @@ type fakeEvaluator struct {
}
// Process rule
func (e *fakeEvaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
func (e *fakeEvaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
return artifacts, nil
}

View File

@ -19,9 +19,9 @@ import (
"time"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -40,7 +40,7 @@ type evaluator struct {
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) (retain []*res.Candidate, err error) {
func (e *evaluator) Process(artifacts []*art.Candidate) (retain []*art.Candidate, err error) {
cutoff := time.Now().Add(time.Duration(e.x*-24) * time.Hour)
for _, a := range artifacts {
if time.Unix(a.PushedTime, 0).UTC().After(cutoff) {

View File

@ -5,8 +5,8 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -38,7 +38,7 @@ func (e *EvaluatorTestSuite) TestNew() {
func (e *EvaluatorTestSuite) TestProcess() {
now := time.Now().UTC()
data := []*res.Candidate{
data := []*art.Candidate{
{PushedTime: now.Add(time.Duration(1*-24) * time.Hour).Unix()},
{PushedTime: now.Add(time.Duration(2*-24) * time.Hour).Unix()},
{PushedTime: now.Add(time.Duration(3*-24) * time.Hour).Unix()},

View File

@ -19,9 +19,9 @@ import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -40,7 +40,7 @@ type evaluator struct {
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
// Sort artifacts by their "active time"
//
// Active time is defined as the selection of c.PulledTime or c.PushedTime,
@ -81,7 +81,7 @@ func New(params rule.Parameters) rule.Evaluator {
}
}
func activeTime(c *res.Candidate) int64 {
func activeTime(c *art.Candidate) int64 {
if c.PulledTime > c.PushedTime {
return c.PulledTime
}

View File

@ -22,18 +22,18 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
artifacts []*art.Candidate
}
func (e *EvaluatorTestSuite) SetupSuite() {
e.artifacts = []*res.Candidate{
e.artifacts = []*art.Candidate{
{PulledTime: 1, PushedTime: 2},
{PulledTime: 3, PushedTime: 4},
{PulledTime: 6, PushedTime: 5},

View File

@ -21,9 +21,9 @@ import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -41,7 +41,7 @@ type evaluator struct {
n int
}
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
sort.Slice(artifacts, func(i, j int) bool {
return artifacts[i].PulledTime > artifacts[j].PulledTime
})

View File

@ -20,8 +20,8 @@ import (
"math/rand"
"testing"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -52,7 +52,7 @@ func (e *EvaluatorTestSuite) TestNew() {
}
func (e *EvaluatorTestSuite) TestProcess() {
data := []*res.Candidate{{PulledTime: 0}, {PulledTime: 1}, {PulledTime: 2}, {PulledTime: 3}, {PulledTime: 4}}
data := []*art.Candidate{{PulledTime: 0}, {PulledTime: 1}, {PulledTime: 2}, {PulledTime: 3}, {PulledTime: 4}}
rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i]
})

View File

@ -21,9 +21,9 @@ import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -42,7 +42,7 @@ type evaluator struct {
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
// The updated proposal does not guarantee the order artifacts are provided, so we have to sort them first
sort.Slice(artifacts, func(i, j int) bool {
return artifacts[i].PushedTime > artifacts[j].PushedTime

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
)
@ -39,7 +39,7 @@ func (e *EvaluatorTestSuite) TestNew() {
}
func (e *EvaluatorTestSuite) TestProcess() {
data := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}, {PushedTime: 4}}
data := []*art.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}, {PushedTime: 4}}
rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i]
})

View File

@ -15,9 +15,9 @@
package nothing
import (
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
@ -28,7 +28,7 @@ const (
type evaluator struct{}
// Process for the "nothing" Evaluator simply returns the input with no error
func (e *evaluator) Process(artifacts []*res.Candidate) (processed []*res.Candidate, err error) {
func (e *evaluator) Process(artifacts []*art.Candidate) (processed []*art.Candidate, err error) {
return processed, err
}

View File

@ -17,8 +17,8 @@ package nothing
import (
"testing"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -36,7 +36,7 @@ func (e *EvaluatorTestSuite) TestNew() {
func (e *EvaluatorTestSuite) TestProcess() {
sut := New(rule.Parameters{})
input := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
input := []*art.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
result, err := sut.Process(input)

View File

@ -0,0 +1,157 @@
// Copyright Project Harbor Authors
//
// 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 api
import (
"github.com/goharbor/harbor/src/pkg/q"
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
)
// Controller provides the related operations of scanner for the upper API.
// All the capabilities of the scanner are defined here.
type Controller interface {
// ListRegistrations returns a list of currently configured scanner registrations.
// Query parameters are optional
//
// Arguments:
// query *q.Query : query parameters
//
// Returns:
// []*scanner.Registration : scanner list of all the matched ones
// error : non nil error if any errors occurred
ListRegistrations(query *q.Query) ([]*scanner.Registration, error)
// CreateRegistration creates a new scanner registration with the given data.
// Returns the scanner registration identifier.
//
// Arguments:
// registration *scanner.Registration : scanner registration to create
//
// Returns:
// string : the generated UUID of the new scanner
// error : non nil error if any errors occurred
CreateRegistration(registration *scanner.Registration) (string, error)
// GetRegistration returns the details of the specified scanner registration.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner
//
// Returns:
// *scanner.Registration : the required scanner
// error : non nil error if any errors occurred
GetRegistration(registrationUUID string) (*scanner.Registration, error)
// RegistrationExists checks if the provided registration is there.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner
//
// Returns:
// true for existing or false for not existing
RegistrationExists(registrationUUID string) bool
// UpdateRegistration updates the specified scanner registration.
//
// Arguments:
// registration *scanner.Registration : scanner registration to update
//
// Returns:
// error : non nil error if any errors occurred
UpdateRegistration(registration *scanner.Registration) error
// DeleteRegistration deletes the specified scanner registration.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner which is going to be deleted
//
// Returns:
// *scanner.Registration : the deleted scanner
// error : non nil error if any errors occurred
DeleteRegistration(registrationUUID string) (*scanner.Registration, error)
// SetDefaultRegistration marks the specified scanner registration as default.
// The implementation is supposed to unset any registration previously set as default.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner which is marked as default
//
// Returns:
// error : non nil error if any errors occurred
SetDefaultRegistration(registrationUUID string) error
// SetRegistrationByProject sets scanner for the given project.
//
// Arguments:
// projectID int64 : the ID of the given project
// scannerID string : the UUID of the the scanner
//
// Returns:
// error : non nil error if any errors occurred
SetRegistrationByProject(projectID int64, scannerID string) error
// GetRegistrationByProject returns the configured scanner registration of the given project or
// the system default registration if exists or `nil` if no system registrations set.
//
// Arguments:
// projectID int64 : the ID of the given project
//
// Returns:
// *scanner.Registration : the default scanner registration
// error : non nil error if any errors occurred
GetRegistrationByProject(projectID int64) (*scanner.Registration, error)
// Ping pings Scanner Adapter to test EndpointURL and Authorization settings.
// The implementation is supposed to call the GetMetadata method on scanner.Client.
// Returns `nil` if connection succeeded, a non `nil` error otherwise.
//
// Arguments:
// registration *scanner.Registration : scanner registration to ping
//
// Returns:
// error : non nil error if any errors occurred
Ping(registration *scanner.Registration) error
// Scan the given artifact
//
// Arguments:
// artifact *res.Artifact : artifact to be scanned
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *scan.Artifact) error
// GetReport gets the reports for the given artifact identified by the digest
//
// Arguments:
// artifact *res.Artifact : the scanned artifact
//
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *scan.Artifact) ([]*dscan.Report, error)
// Get the scan log for the specified artifact with the given digest
//
// Arguments:
// digest string : the digest of the artifact
//
// Returns:
// []byte : the log text stream
// error : non nil error if any errors occurred
GetScanLog(digest string) ([]byte, error)
}

View File

@ -0,0 +1,287 @@
// Copyright Project Harbor Authors
//
// 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 api
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ControllerTestSuite is test suite to test the basic api controller.
type ControllerTestSuite struct {
suite.Suite
c *basicController
mMgr *MockScannerManager
mMeta *MockProMetaManager
sample *scanner.Registration
}
// TestController is the entry of controller test suite
func TestController(t *testing.T) {
suite.Run(t, new(ControllerTestSuite))
}
// SetupSuite prepares env for the controller test suite
func (suite *ControllerTestSuite) SetupSuite() {
suite.mMgr = new(MockScannerManager)
suite.mMeta = new(MockProMetaManager)
suite.c = &basicController{
manager: suite.mMgr,
proMetaMgr: suite.mMeta,
}
suite.sample = &scanner.Registration{
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
}
// Clear test case
func (suite *ControllerTestSuite) TearDownTest() {
suite.sample.UUID = ""
}
// TestListRegistrations tests ListRegistrations
func (suite *ControllerTestSuite) TestListRegistrations() {
query := &q.Query{
PageSize: 10,
PageNumber: 1,
}
suite.sample.UUID = "uuid"
l := []*scanner.Registration{suite.sample}
suite.mMgr.On("List", query).Return(l, nil)
rl, err := suite.c.ListRegistrations(query)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(rl))
}
// TestCreateRegistration tests CreateRegistration
func (suite *ControllerTestSuite) TestCreateRegistration() {
suite.mMgr.On("Create", suite.sample).Return("uuid", nil)
uid, err := suite.mMgr.Create(suite.sample)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), uid, "uuid")
}
// TestGetRegistration tests GetRegistration
func (suite *ControllerTestSuite) TestGetRegistration() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
rr, err := suite.c.GetRegistration("uuid")
require.NoError(suite.T(), err)
assert.NotNil(suite.T(), rr)
assert.Equal(suite.T(), "forUT", rr.Name)
}
// TestRegistrationExists tests RegistrationExists
func (suite *ControllerTestSuite) TestRegistrationExists() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
exists := suite.c.RegistrationExists("uuid")
assert.Equal(suite.T(), true, exists)
suite.mMgr.On("Get", "uuid2").Return(nil, nil)
exists = suite.c.RegistrationExists("uuid2")
assert.Equal(suite.T(), false, exists)
}
// TestUpdateRegistration tests UpdateRegistration
func (suite *ControllerTestSuite) TestUpdateRegistration() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Update", suite.sample).Return(nil)
err := suite.c.UpdateRegistration(suite.sample)
require.NoError(suite.T(), err)
}
// TestDeleteRegistration tests DeleteRegistration
func (suite *ControllerTestSuite) TestDeleteRegistration() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
suite.mMgr.On("Delete", "uuid").Return(nil)
r, err := suite.c.DeleteRegistration("uuid")
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "forUT", r.Name)
}
// TestSetDefaultRegistration tests SetDefaultRegistration
func (suite *ControllerTestSuite) TestSetDefaultRegistration() {
suite.mMgr.On("SetAsDefault", "uuid").Return(nil)
err := suite.c.SetDefaultRegistration("uuid")
require.NoError(suite.T(), err)
}
// TestSetRegistrationByProject tests SetRegistrationByProject
func (suite *ControllerTestSuite) TestSetRegistrationByProject() {
m := make(map[string]string, 1)
mm := make(map[string]string, 1)
mmm := make(map[string]string, 1)
mm[proScannerMetaKey] = "uuid"
mmm[proScannerMetaKey] = "uuid2"
var pid, pid2 int64 = 1, 2
// not set before
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil)
suite.mMeta.On("Add", pid, mm).Return(nil)
err := suite.c.SetRegistrationByProject(pid, "uuid")
require.NoError(suite.T(), err)
// Set before
suite.mMeta.On("Get", pid2, []string{proScannerMetaKey}).Return(mm, nil)
suite.mMeta.On("Update", pid2, mmm).Return(nil)
err = suite.c.SetRegistrationByProject(pid2, "uuid2")
require.NoError(suite.T(), err)
}
// TestGetRegistrationByProject tests GetRegistrationByProject
func (suite *ControllerTestSuite) TestGetRegistrationByProject() {
m := make(map[string]string, 1)
m[proScannerMetaKey] = "uuid"
// Configured at project level
var pid int64 = 1
suite.sample.UUID = "uuid"
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil)
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
r, err := suite.c.GetRegistrationByProject(pid)
require.NoError(suite.T(), err)
require.Equal(suite.T(), "forUT", r.Name)
// Not configured at project level, return system default
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(nil, nil)
suite.mMgr.On("GetDefault").Return(suite.sample, nil)
r, err = suite.c.GetRegistrationByProject(pid)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "forUT", r.Name)
}
// MockScannerManager is mock of the scanner manager
type MockScannerManager struct {
mock.Mock
}
// List ...
func (m *MockScannerManager) List(query *q.Query) ([]*scanner.Registration, error) {
args := m.Called(query)
return args.Get(0).([]*scanner.Registration), args.Error(1)
}
// Create ...
func (m *MockScannerManager) Create(registration *scanner.Registration) (string, error) {
args := m.Called(registration)
return args.String(0), args.Error(1)
}
// Get ...
func (m *MockScannerManager) Get(registrationUUID string) (*scanner.Registration, error) {
args := m.Called(registrationUUID)
r := args.Get(0)
if r == nil {
return nil, args.Error(1)
}
return r.(*scanner.Registration), args.Error(1)
}
// Update ...
func (m *MockScannerManager) Update(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// Delete ...
func (m *MockScannerManager) Delete(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// SetAsDefault ...
func (m *MockScannerManager) SetAsDefault(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// GetDefault ...
func (m *MockScannerManager) GetDefault() (*scanner.Registration, error) {
args := m.Called()
return args.Get(0).(*scanner.Registration), args.Error(1)
}
// MockProMetaManager is the mock of the ProjectMetadataManager
type MockProMetaManager struct {
mock.Mock
}
// Add ...
func (m *MockProMetaManager) Add(projectID int64, meta map[string]string) error {
args := m.Called(projectID, meta)
return args.Error(0)
}
// Delete ...
func (m *MockProMetaManager) Delete(projecdtID int64, meta ...string) error {
args := m.Called(projecdtID, meta)
return args.Error(0)
}
// Update ...
func (m *MockProMetaManager) Update(projectID int64, meta map[string]string) error {
args := m.Called(projectID, meta)
return args.Error(0)
}
// Get ...
func (m *MockProMetaManager) Get(projectID int64, meta ...string) (map[string]string, error) {
args := m.Called(projectID, meta)
return args.Get(0).(map[string]string), args.Error(1)
}
// List ...
func (m *MockProMetaManager) List(name, value string) ([]*models.ProjectMetadata, error) {
args := m.Called(name, value)
return args.Get(0).([]*models.ProjectMetadata), args.Error(1)
}

View File

@ -0,0 +1,194 @@
// Copyright Project Harbor Authors
//
// 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 api
import (
"github.com/goharbor/harbor/src/core/promgr/metamgr"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/q"
rscanner "github.com/goharbor/harbor/src/pkg/scan/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
"github.com/pkg/errors"
)
const (
proScannerMetaKey = "projectScanner"
)
// DefaultController is a singleton api controller for plug scanners
var DefaultController = New()
// New a basic controller
func New() Controller {
return &basicController{
manager: rscanner.New(),
proMetaMgr: metamgr.NewDefaultProjectMetadataManager(),
}
}
// basicController is default implementation of api.Controller interface
type basicController struct {
// managers for managing the scanner registrations
manager rscanner.Manager
// for operating the project level configured scanner
proMetaMgr metamgr.ProjectMetadataManager
// controller for scan actions
c scan.Controller
// Client
}
// ListRegistrations ...
func (bc *basicController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) {
return bc.manager.List(query)
}
// CreateRegistration ...
func (bc *basicController) CreateRegistration(registration *scanner.Registration) (string, error) {
// TODO: Get metadata from the adapter service first
l, err := bc.manager.List(nil)
if err != nil {
return "", err
}
if len(l) == 0 && !registration.IsDefault {
// Mark the 1st as default automatically
registration.IsDefault = true
}
return bc.manager.Create(registration)
}
// GetRegistration ...
func (bc *basicController) GetRegistration(registrationUUID string) (*scanner.Registration, error) {
return bc.manager.Get(registrationUUID)
}
// RegistrationExists ...
func (bc *basicController) RegistrationExists(registrationUUID string) bool {
registration, err := bc.manager.Get(registrationUUID)
// Just logged when an error occurred
if err != nil {
logger.Errorf("Check existence of registration error: %s", err)
}
return !(err == nil && registration == nil)
}
// UpdateRegistration ...
func (bc *basicController) UpdateRegistration(registration *scanner.Registration) error {
return bc.manager.Update(registration)
}
// SetDefaultRegistration ...
func (bc *basicController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) {
registration, err := bc.manager.Get(registrationUUID)
if registration == nil && err == nil {
// Not found
return nil, nil
}
if err := bc.manager.Delete(registrationUUID); err != nil {
return nil, errors.Wrap(err, "delete registration")
}
return registration, nil
}
// SetDefaultRegistration ...
func (bc *basicController) SetDefaultRegistration(registrationUUID string) error {
return bc.manager.SetAsDefault(registrationUUID)
}
// SetRegistrationByProject ...
func (bc *basicController) SetRegistrationByProject(projectID int64, registrationID string) error {
if projectID == 0 {
return errors.New("invalid project ID")
}
if len(registrationID) == 0 {
return errors.New("missing scanner UUID")
}
// Only keep the UUID in the metadata of the given project
// Scanner metadata existing?
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
if err != nil {
return errors.Wrap(err, "set project scanner")
}
// Update if exists
if len(m) > 0 {
// Compare and set new
if registrationID != m[proScannerMetaKey] {
m[proScannerMetaKey] = registrationID
if err := bc.proMetaMgr.Update(projectID, m); err != nil {
return errors.Wrap(err, "set project scanner")
}
}
} else {
meta := make(map[string]string, 1)
meta[proScannerMetaKey] = registrationID
if err := bc.proMetaMgr.Add(projectID, meta); err != nil {
return errors.Wrap(err, "set project scanner")
}
}
return nil
}
// GetRegistrationByProject ...
func (bc *basicController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) {
if projectID == 0 {
return nil, errors.New("invalid project ID")
}
// First, get it from the project metadata
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
if err != nil {
return nil, errors.Wrap(err, "get project scanner")
}
if len(m) > 0 {
if registrationID, ok := m[proScannerMetaKey]; ok && len(registrationID) > 0 {
registration, err := bc.manager.Get(registrationID)
if err != nil {
return nil, errors.Wrap(err, "get project scanner")
}
if registration == nil {
// Not found
// Might be deleted by the admin, the project scanner ID reference should be cleared
if err := bc.proMetaMgr.Delete(projectID, proScannerMetaKey); err != nil {
return nil, errors.Wrap(err, "get project scanner")
}
} else {
return registration, nil
}
}
}
// Second, get the default one
registration, err := bc.manager.GetDefault()
// TODO: Check status by the client later
return registration, err
}
// Ping ...
func (bc *basicController) Ping(registration *scanner.Registration) error {
return nil
}

View File

@ -0,0 +1,35 @@
// Copyright Project Harbor Authors
//
// 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 api
import (
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
)
// Scan ...
func (bc *basicController) Scan(artifact *scan.Artifact) error {
return nil
}
// GetReport ...
func (bc *basicController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
return nil, nil
}
// GetScanLog ...
func (bc *basicController) GetScanLog(digest string) ([]byte, error) {
return nil, nil
}

View File

@ -0,0 +1,43 @@
// Copyright Project Harbor Authors
//
// 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 scan
import "time"
// Report of the scan
// Identified by the `digest` and `endpoint_id`
type Report struct {
ID int64 `orm:"pk;auto;column(id)"`
Digest string `orm:"column(digest)"`
ReregistrationID string `orm:"column(registration_id)"`
JobID string `orm:"column(job_id)"`
Status string `orm:"column(status)"`
StatusCode int `orm:"column(status_code)"`
Report string `orm:"column(report);type(json)"`
StartTime time.Time `orm:"column(start_time);auto_now_add;type(datetime)"`
EndTime time.Time `orm:"column(end_time);type(datetime)"`
}
// TableName for Report
func (r *Report) TableName() string {
return "scanner_report"
}
// TableUnique for Report
func (r *Report) TableUnique() [][]string {
return [][]string{
{"digest", "registration_id"},
}
}

View File

@ -0,0 +1,120 @@
// Copyright Project Harbor Authors
//
// 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 scanner
import (
"encoding/json"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
)
// Registration represents a named configuration for invoking a scanner via its adapter.
// UUID will be used to track the scanner.Endpoint as unique ID
type Registration struct {
// Basic information
// int64 ID is kept for being aligned with previous DB schema
ID int64 `orm:"pk;auto;column(id)" json:"-"`
UUID string `orm:"unique;column(uuid)" json:"uuid"`
Name string `orm:"unique;column(name);size(128)" json:"name"`
Description string `orm:"column(description);null;size(1024)" json:"description"`
URL string `orm:"column(url);unique;size(512)" json:"url"`
Disabled bool `orm:"column(disabled);default(true)" json:"disabled"`
IsDefault bool `orm:"column(is_default);default(false)" json:"is_default"`
Health bool `orm:"-" json:"health"`
// Authentication settings
// "None","Basic" and "Bearer" can be supported
Auth string `orm:"column(auth);size(16)" json:"auth"`
AccessCredential string `orm:"column(access_cred);null;size(512)" json:"access_credential,omitempty"`
// Http connection settings
SkipCertVerify bool `orm:"column(skip_cert_verify);default(false)" json:"skip_certVerify"`
// Adapter settings
Adapter string `orm:"column(adapter);size(128)" json:"adapter"`
Vendor string `orm:"column(vendor);size(128)" json:"vendor"`
Version string `orm:"column(version);size(32)" json:"version"`
// Timestamps
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
}
// TableName for Endpoint
func (r *Registration) TableName() string {
return "scanner_registration"
}
// FromJSON parses json data
func (r *Registration) FromJSON(jsonData string) error {
if len(jsonData) == 0 {
return errors.New("empty json data to parse")
}
return json.Unmarshal([]byte(jsonData), r)
}
// ToJSON marshals endpoint to JSON data
func (r *Registration) ToJSON() (string, error) {
data, err := json.Marshal(r)
if err != nil {
return "", err
}
return string(data), nil
}
// Validate endpoint
func (r *Registration) Validate(checkUUID bool) error {
if checkUUID && len(r.UUID) == 0 {
return errors.New("malformed endpoint")
}
if len(r.Name) == 0 {
return errors.New("missing registration name")
}
err := checkURL(r.URL)
if err != nil {
return errors.Wrap(err, "scanner registration validate")
}
if len(r.Adapter) == 0 ||
len(r.Vendor) == 0 ||
len(r.Version) == 0 {
return errors.Errorf("missing adapter settings in registration %s:%s", r.Name, r.URL)
}
return nil
}
// Check the registration URL with url package
func checkURL(u string) error {
if len(strings.TrimSpace(u)) == 0 {
return errors.New("empty url")
}
uri, err := url.Parse(u)
if err == nil {
if uri.Scheme != "http" && uri.Scheme != "https" {
err = errors.New("invalid scheme")
}
}
return err
}

View File

@ -0,0 +1,87 @@
// Copyright Project Harbor Authors
//
// 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 scanner
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ModelTestSuite tests the utility functions of the model
type ModelTestSuite struct {
suite.Suite
}
// TestModel is the entry of the model test suite
func TestModel(t *testing.T) {
suite.Run(t, new(ModelTestSuite))
}
// TestJSON tests the marshal and unmarshal functions
func (suite *ModelTestSuite) TestJSON() {
r := &Registration{
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
json, err := r.ToJSON()
require.NoError(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) {
success = len(json) > 0
return
})
r2 := &Registration{}
err = r2.FromJSON(json)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), "forUT", r2.Name)
}
// TestValidate tests the validate function
func (suite *ModelTestSuite) TestValidate() {
r := &Registration{}
err := r.Validate(true)
require.Error(suite.T(), err)
r.UUID = "uuid"
err = r.Validate(true)
require.Error(suite.T(), err)
r.Name = "forUT"
err = r.Validate(true)
require.Error(suite.T(), err)
r.URL = "a.b.c"
err = r.Validate(true)
require.Error(suite.T(), err)
r.URL = "http://a.b.c"
err = r.Validate(true)
require.Error(suite.T(), err)
r.Adapter = "Clair"
r.Vendor = "Harbor"
r.Version = "0.1.0"
err = r.Validate(true)
require.NoError(suite.T(), err)
}

View File

@ -0,0 +1,147 @@
// Copyright Project Harbor Authors
//
// 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 scanner
import (
"fmt"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/pkg/errors"
)
func init() {
orm.RegisterModel(new(Registration))
}
// AddRegistration adds a new registration
func AddRegistration(r *Registration) (int64, error) {
o := dao.GetOrmer()
return o.Insert(r)
}
// GetRegistration gets the specified registration
func GetRegistration(UUID string) (*Registration, error) {
e := &Registration{}
o := dao.GetOrmer()
qs := o.QueryTable(new(Registration))
if err := qs.Filter("uuid", UUID).One(e); err != nil {
if err == orm.ErrNoRows {
// Not existing case
return nil, nil
}
return nil, err
}
return e, nil
}
// UpdateRegistration update the specified registration
func UpdateRegistration(r *Registration, cols ...string) error {
o := dao.GetOrmer()
count, err := o.Update(r, cols...)
if err != nil {
return err
}
if count == 0 {
return errors.Errorf("no item with UUID %s is updated", r.UUID)
}
return nil
}
// DeleteRegistration deletes the registration with the specified UUID
func DeleteRegistration(UUID string) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
// delete with query way
count, err := qt.Filter("uuid", UUID).Delete()
if err != nil {
return err
}
if count == 0 {
return errors.Errorf("no item with UUID %s is deleted", UUID)
}
return nil
}
// ListRegistrations lists all the existing registrations
func ListRegistrations(query *q.Query) ([]*Registration, error) {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
if query != nil {
if len(query.Keywords) > 0 {
for k, v := range query.Keywords {
qt = qt.Filter(fmt.Sprintf("%s__icontains", k), v)
}
}
if query.PageNumber > 0 && query.PageSize > 0 {
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
}
l := make([]*Registration, 0)
_, err := qt.All(&l)
return l, err
}
// SetDefaultRegistration sets the specified registration as default one
func SetDefaultRegistration(UUID string) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
_, err := qt.Filter("is_default", true).Update(orm.Params{
"is_default": false,
})
if err != nil {
return err
}
qt2 := o.QueryTable(new(Registration))
_, err = qt2.Filter("uuid", UUID).Update(orm.Params{
"is_default": true,
})
return err
}
// GetDefaultRegistration gets the default registration
func GetDefaultRegistration() (*Registration, error) {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
e := &Registration{}
if err := qt.Filter("is_default", true).One(e); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,144 @@
// Copyright Project Harbor Authors
//
// 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 scanner
import (
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// RegistrationDAOTestSuite is test suite of testing registration DAO
type RegistrationDAOTestSuite struct {
suite.Suite
registrationID string
}
// TestRegistrationDAO is entry of test cases
func TestRegistrationDAO(t *testing.T) {
suite.Run(t, new(RegistrationDAOTestSuite))
}
// SetupSuite prepare testing env for the suite
func (suite *RegistrationDAOTestSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
}
// SetupTest prepare stuff for test cases
func (suite *RegistrationDAOTestSuite) SetupTest() {
suite.registrationID = uuid.New().String()
r := &Registration{
UUID: suite.registrationID,
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
_, err := AddRegistration(r)
require.NoError(suite.T(), err, "add new registration")
}
// TearDownTest clears all the stuff of test cases
func (suite *RegistrationDAOTestSuite) TearDownTest() {
err := DeleteRegistration(suite.registrationID)
require.NoError(suite.T(), err, "clear registration")
}
// TestGet tests get registration
func (suite *RegistrationDAOTestSuite) TestGet() {
// Found
r, err := GetRegistration(suite.registrationID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), r.Name, "forUT")
// Not found
re, err := GetRegistration("not_found")
require.NoError(suite.T(), err)
require.Nil(suite.T(), re)
}
// TestUpdate tests update registration
func (suite *RegistrationDAOTestSuite) TestUpdate() {
r, err := GetRegistration(suite.registrationID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
r.Disabled = true
r.IsDefault = true
r.URL = "http://updated.registration.com"
err = UpdateRegistration(r)
require.NoError(suite.T(), err, "update registration")
r, err = GetRegistration(suite.registrationID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), true, r.Disabled)
assert.Equal(suite.T(), true, r.IsDefault)
assert.Equal(suite.T(), "http://updated.registration.com", r.URL)
}
// TestList tests list registrations
func (suite *RegistrationDAOTestSuite) TestList() {
// no query
l, err := ListRegistrations(nil)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
// with query and found items
keywords := make(map[string]string)
keywords["adapter"] = "Clair"
l, err = ListRegistrations(&q.Query{
PageSize: 5,
PageNumber: 1,
Keywords: keywords,
})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
// With query and not found items
keywords["adapter"] = "Micro scanner"
l, err = ListRegistrations(&q.Query{
Keywords: keywords,
})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 0, len(l))
}
// TestDefault tests set/get default
func (suite *RegistrationDAOTestSuite) TestDefault() {
dr, err := GetDefaultRegistration()
require.NoError(suite.T(), err, "not found")
require.Nil(suite.T(), dr)
err = SetDefaultRegistration(suite.registrationID)
require.NoError(suite.T(), err)
dr, err = GetDefaultRegistration()
require.NoError(suite.T(), err)
require.NotNil(suite.T(), dr)
}

View File

@ -0,0 +1,131 @@
// Copyright Project Harbor Authors
//
// 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 scanner
import (
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/google/uuid"
"github.com/pkg/errors"
)
// Manager defines the related scanner API endpoints
type Manager interface {
// List returns a list of currently configured scanner registrations.
// Query parameters are optional
List(query *q.Query) ([]*scanner.Registration, error)
// Create creates a new scanner registration with the given data.
// Returns the scanner registration identifier.
Create(registration *scanner.Registration) (string, error)
// Get returns the details of the specified scanner registration.
Get(registrationUUID string) (*scanner.Registration, error)
// Update updates the specified scanner registration.
Update(registration *scanner.Registration) error
// Delete deletes the specified scanner registration.
Delete(registrationUUID string) error
// SetAsDefault marks the specified scanner registration as default.
// The implementation is supposed to unset any registration previously set as default.
SetAsDefault(registrationUUID string) error
// GetDefault returns the default scanner registration or `nil` if there are no registrations configured.
GetDefault() (*scanner.Registration, error)
}
// basicManager is the default implementation of Manager
type basicManager struct{}
// New a basic manager
func New() Manager {
return &basicManager{}
}
// Create ...
func (bm *basicManager) Create(registration *scanner.Registration) (string, error) {
if registration == nil {
return "", errors.New("nil endpoint to create")
}
// Inject new UUID
uid, err := uuid.NewUUID()
if err != nil {
return "", errors.Wrap(err, "new UUID: create registration")
}
registration.UUID = uid.String()
if err := registration.Validate(true); err != nil {
return "", errors.Wrap(err, "create registration")
}
if _, err := scanner.AddRegistration(registration); err != nil {
return "", errors.Wrap(err, "dao: create registration")
}
return uid.String(), nil
}
// Get ...
func (bm *basicManager) Get(registrationUUID string) (*scanner.Registration, error) {
if len(registrationUUID) == 0 {
return nil, errors.New("empty uuid of registration")
}
return scanner.GetRegistration(registrationUUID)
}
// Update ...
func (bm *basicManager) Update(registration *scanner.Registration) error {
if registration == nil {
return errors.New("nil endpoint to update")
}
if err := registration.Validate(true); err != nil {
return errors.Wrap(err, "update endpoint")
}
return scanner.UpdateRegistration(registration)
}
// Delete ...
func (bm *basicManager) Delete(registrationUUID string) error {
if len(registrationUUID) == 0 {
return errors.New("empty UUID to delete")
}
return scanner.DeleteRegistration(registrationUUID)
}
// List ...
func (bm *basicManager) List(query *q.Query) ([]*scanner.Registration, error) {
return scanner.ListRegistrations(query)
}
// SetAsDefault ...
func (bm *basicManager) SetAsDefault(registrationUUID string) error {
if len(registrationUUID) == 0 {
return errors.New("empty UUID to set default")
}
return scanner.SetDefaultRegistration(registrationUUID)
}
// GetDefault ...
func (bm *basicManager) GetDefault() (*scanner.Registration, error) {
return scanner.GetDefaultRegistration()
}

View File

@ -0,0 +1,115 @@
// Copyright Project Harbor Authors
//
// 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 scanner
import (
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// BasicManagerTestSuite tests the basic manager
type BasicManagerTestSuite struct {
suite.Suite
mgr Manager
sampleUUID string
}
// TestBasicManager is the entry of BasicManagerTestSuite
func TestBasicManager(t *testing.T) {
suite.Run(t, new(BasicManagerTestSuite))
}
// SetupSuite prepares env for test suite
func (suite *BasicManagerTestSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
suite.mgr = New()
r := &scanner.Registration{
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
uid, err := suite.mgr.Create(r)
require.NoError(suite.T(), err)
suite.sampleUUID = uid
}
// TearDownSuite clears env for test suite
func (suite *BasicManagerTestSuite) TearDownSuite() {
err := suite.mgr.Delete(suite.sampleUUID)
require.NoError(suite.T(), err, "delete registration")
}
// TestList tests list registrations
func (suite *BasicManagerTestSuite) TestList() {
m := make(map[string]string, 1)
m["name"] = "forUT"
l, err := suite.mgr.List(&q.Query{
PageNumber: 1,
PageSize: 10,
Keywords: m,
})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
}
// TestGet tests get registration
func (suite *BasicManagerTestSuite) TestGet() {
r, err := suite.mgr.Get(suite.sampleUUID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "forUT", r.Name)
}
// TestUpdate tests update registration
func (suite *BasicManagerTestSuite) TestUpdate() {
r, err := suite.mgr.Get(suite.sampleUUID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
r.URL = "https://updated.com"
err = suite.mgr.Update(r)
require.NoError(suite.T(), err)
r, err = suite.mgr.Get(suite.sampleUUID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "https://updated.com", r.URL)
}
// TestDefault tests get/set default registration
func (suite *BasicManagerTestSuite) TestDefault() {
err := suite.mgr.SetAsDefault(suite.sampleUUID)
require.NoError(suite.T(), err)
dr, err := suite.mgr.GetDefault()
require.NoError(suite.T(), err)
require.NotNil(suite.T(), dr)
assert.Equal(suite.T(), true, dr.IsDefault)
}

View File

@ -0,0 +1,48 @@
// Copyright Project Harbor Authors
//
// 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 scan
import "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
// Options object for the scan action
type Options struct{}
// Option for scan action
type Option interface {
// Apply option to the passing in options
Apply(options *Options) error
}
// Controller defines operations for scan controlling
type Controller interface {
// Scan the given artifact
//
// Arguments:
// artifact *res.Artifact : artifact to be scanned
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *Artifact, options ...Option) error
// GetReport gets the reports for the given artifact identified by the digest
//
// Arguments:
// artifact *res.Artifact : the scanned artifact
//
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *Artifact) ([]*scan.Report, error)
}

View File

@ -0,0 +1,46 @@
// Copyright Project Harbor Authors
//
// 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 scan
// Artifact represents an artifact stored in Registry.
type Artifact struct {
// The full name of a Harbor repository containing the artifact, including the namespace.
// For example, `library/oracle/nosql`.
Repository string
// The artifact's digest, consisting of an algorithm and hex portion.
// For example, `sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b`,
// represents sha256 based digest.
Digest string
// The mime type of the scanned artifact
MimeType string
}
// Registry represents Registry connection settings.
type Registry struct {
// A base URL of the Docker Registry v2 API exposed by Harbor.
URL string
// An optional value of the HTTP Authorization header sent with each request to the Docker Registry v2 API.
// For example, `Bearer: JWTTOKENGOESHERE`.
Authorization string
}
// Request represents a structure that is sent to a Scanner Adapter to initiate artifact scanning.
// Conducts all the details required to pull the artifact from a Harbor registry.
type Request struct {
// Connection settings for the Docker Registry v2 API exposed by Harbor.
Registry *Registry
// Artifact to be scanned.
Artifact *Artifact
}

View File

@ -26,6 +26,7 @@
"node_modules/@clr/ui/clr-ui.min.css",
"node_modules/swagger-ui/dist/swagger-ui.css",
"node_modules/prismjs/themes/prism-solarizedlight.css",
"src/global.scss",
"src/styles.css"
],
"scripts": [

View File

@ -4,11 +4,6 @@
"deleteDestPath": false,
"lib": {
"entryFile": "index.ts",
"externals": {
"@ngx-translate/core": "ngx-translate-core",
"@ngx-translate/core/index": "ngx-translate-core",
"ngx-markdown": "ngx-markdown"
},
"umdModuleIds": {
"@clr/angular" : "angular",
"ngx-markdown" : "ngxMarkdown",

View File

@ -3,11 +3,6 @@
"dest": "./dist",
"lib": {
"entryFile": "index.ts",
"externals": {
"@ngx-translate/core": "ngx-translate-core",
"@ngx-translate/core/index": "ngx-translate-core",
"ngx-markdown": "ngx-markdown"
},
"umdModuleIds": {
"@clr/angular" : "angular",
"ngx-markdown" : "ngxMarkdown",

5
src/portal/lib/package-lock.json generated Normal file
View File

@ -0,0 +1,5 @@
{
"name": "@harbor/ui",
"version": "1.10.0",
"lockfileVersion": 1
}

View File

@ -1,7 +1,7 @@
{
"name": "@harbor/ui",
"version": "1.9.0",
"description": "Harbor shared UI components based on Clarity and Angular7",
"version": "1.10.0",
"description": "Harbor shared UI components based on Clarity and Angular8",
"author": "CNCF",
"module": "index.js",
"main": "bundles/harborui.umd.min.js",
@ -19,26 +19,26 @@
},
"homepage": "https://github.com/vmware/harbor#readme",
"peerDependencies": {
"@angular/animations": "^7.1.3",
"@angular/common": "^7.1.3",
"@angular/compiler": "^7.1.3",
"@angular/core": "^7.1.3",
"@angular/forms": "^7.1.3",
"@angular/http": "^7.1.3",
"@angular/platform-browser": "^7.1.3",
"@angular/platform-browser-dynamic": "^7.1.3",
"@angular/router": "^7.1.3",
"@angular/animations": "^8.2.0",
"@angular/common": "^8.2.0",
"@angular/compiler": "^8.2.0",
"@angular/core": "^8.2.0",
"@angular/forms": "^8.2.0",
"@angular/http": "^8.2.0",
"@angular/platform-browser": "^8.2.0",
"@angular/platform-browser-dynamic": "^8.2.0",
"@angular/router": "^8.2.0",
"@ngx-translate/core": "^10.0.2",
"@ngx-translate/http-loader": "^3.0.1",
"@webcomponents/custom-elements": "^1.1.3",
"@clr/angular": "^1.0.0",
"@clr/ui": "^1.0.0",
"@clr/icons": "^1.0.0",
"@clr/angular": "^2.1.0",
"@clr/icons": "^2.1.0",
"@clr/ui": "^2.1.0",
"core-js": "^2.5.4",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",
"ngx-markdown": "^6.2.0",
"ngx-markdown": "^8.1.0",
"rxjs": "^6.3.3",
"ts-helpers": "^1.1.1",
"web-animations-js": "^2.2.1",

View File

@ -100,6 +100,7 @@ export class Configuration {
oidc_scope?: StringValueItem;
count_per_project: NumberValueItem;
storage_per_project: NumberValueItem;
cfg_expiration: NumberValueItem;
public constructor() {
this.auth_mode = new StringValueItem("db_auth", true);
this.project_creation_restriction = new StringValueItem("everyone", true);

View File

@ -9,6 +9,7 @@ import { GcViewModelFactory } from './gc.viewmodel.factory';
import { CronScheduleComponent } from '../../cron-schedule/cron-schedule.component';
import { CronTooltipComponent } from "../../cron-schedule/cron-tooltip/cron-tooltip.component";
import { of } from 'rxjs';
import { GcJobData } from './gcLog';
describe('GcComponent', () => {
let component: GcComponent;
@ -18,13 +19,17 @@ describe('GcComponent', () => {
systemInfoEndpoint: "/api/system/gc"
};
let mockSchedule = [];
let mockJobs = [
let mockJobs: GcJobData[] = [
{
id: 22222,
schedule: null,
job_status: 'string',
creation_time: new Date(),
update_time: new Date(),
creation_time: new Date().toDateString(),
update_time: new Date().toDateString(),
job_name: 'string',
job_kind: 'string',
job_uuid: 'string',
delete: false
}
];
let spySchedule: jasmine.Spy;

View File

@ -32,7 +32,7 @@ export class GcComponent implements OnInit {
getText = 'CONFIG.GC';
getLabelCurrent = 'GC.CURRENT_SCHEDULE';
@Output() loadingGcStatus = new EventEmitter<boolean>();
@ViewChild(CronScheduleComponent)
@ViewChild(CronScheduleComponent, {static: false})
CronScheduleComponent: CronScheduleComponent;
constructor(
private gcRepoService: GcRepoService,

View File

@ -4,79 +4,73 @@
<div class="modal-body">
<label>{{defaultTextsObj.setQuota}}</label>
<form #quotaForm="ngForm" class="clr-form-compact"
<form #quotaForm="ngForm" class=" clr-form clr-form-horizontal"
[class.clr-form-compact-common]="!defaultTextsObj.isSystemDefaultQuota">
<div class="form-group">
<label for="count" class="required">{{ defaultTextsObj.countQuota | translate}}</label>
<label for="count" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right mr-3px"
[class.invalid]="countInput.invalid && (countInput.dirty || countInput.touched)">
<input name="count" type="text" #countInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.countLimit" pattern="(^-1$)|(^([1-9]+)([0-9]+)*$)" required id="count"
size="40">
<span class="tooltip-content">
{{ 'PROJECT.COUNT_QUOTA_TIP' | translate }}
</span>
</label>
<div class="select-div"></div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right mr-0">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.warning]="isWarningColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)"
[class.danger]="isDangerColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)">
<progress value="{{countInput.invalid || +quotaHardLimitValue.countLimit===-1?0:quotaHardLimitValue.countUsed}}"
max="{{countInput.invalid?100:quotaHardLimitValue.countLimit}}" data-displayval="100%"></progress>
</div>
<label class="progress-label">{{ quotaHardLimitValue?.countUsed }} {{ 'QUOTA.OF' | translate }}
{{ countInput?.valid?+quotaHardLimitValue?.countLimit===-1 ? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.countLimit:('QUOTA.INVALID_INPUT' | translate)}}
<clr-input-container>
<label class="left-label required" for="storage">{{ defaultTextsObj?.storageQuota | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</clr-tooltip-content>
</clr-tooltip>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.warning]="isWarningColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)"
[class.danger]="isDangerColor(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)">
<progress value="{{countInput.invalid || +quotaHardLimitValue.countLimit===-1?0:quotaHardLimitValue.countUsed}}"
max="{{countInput.invalid?100:quotaHardLimitValue.countLimit}}" data-displayval="100%"></progress>
</div>
<label class="progress-label">{{ quotaHardLimitValue?.countUsed }} {{ 'QUOTA.OF' | translate }}
{{ countInput?.valid?+quotaHardLimitValue?.countLimit===-1 ? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.countLimit:('QUOTA.INVALID_INPUT' | translate)}}
</label>
</div>
</label>
</div>
</div>
<div class="form-group">
<label for="storage" class="required">{{ defaultTextsObj?.storageQuota | translate}}</label>
<label for="storage" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-lg mr-3px tooltip-top-right"
[class.invalid]="(storageInput.invalid && (storageInput.dirty || storageInput.touched))||storageInput.errors">
<input name="storage" type="text" #storageInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.storageLimit"
id="storage" size="40">
<span class="tooltip-content">
{{ 'PROJECT.STORAGE_QUOTA_TIP' | translate }}
</span>
</label>
<div class="select-div">
<select clrSelect name="storageUnit" [(ngModel)]="quotaHardLimitValue.storageUnit">
<ng-template ngFor let-quotaUnit [ngForOf]="quotaUnits" let-i="index">
<option *ngIf="i>1" [value]="quotaUnit.UNIT">{{ quotaUnit?.UNIT }}</option>
</ng-template>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right mr-0">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.danger]="isDangerColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)"
[class.warning]="isWarningColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)">
<progress value="{{storageInput.invalid || +quotaHardLimitValue.storageLimit === -1 ?0:quotaHardLimitValue.storageUsed}}"
max="{{storageInput.invalid?0:getByte(+quotaHardLimitValue.storageLimit, quotaHardLimitValue.storageUnit)}}"
data-displayval="100%"></progress>
</div>
<label class="progress-label">
<!-- the comments of progress , when storageLimit !=-1 get integet and unit in hard storage and used storage;and the unit of used storage <= the unit of hard storage
the other : get suitable number and unit-->
{{ +quotaHardLimitValue.storageLimit !== -1 ?(getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partNumberUsed
+ getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partCharacterUsed) : getSuitableUnit(quotaHardLimitValue.storageUsed)}}
{{ 'QUOTA.OF' | translate }}
{{ storageInput?.valid? +quotaHardLimitValue?.storageLimit ===-1? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.storageLimit :('QUOTA.INVALID_INPUT' | translate)}}
{{+quotaHardLimitValue?.storageLimit ===-1?'':quotaHardLimitValue?.storageUnit }}
<input clrInput type="text" name="count" #countInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.countLimit" pattern="(^-1$)|(^([1-9]+)([0-9]+)*$)" required id="count"
size="40" />
<clr-control-error>{{ 'PROJECT.COUNT_QUOTA_TIP' | translate }}</clr-control-error>
</clr-input-container>
<clr-input-container>
<label for="count" class="left-label required">{{ defaultTextsObj.countQuota | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</clr-tooltip-content>
</clr-tooltip>
<div class="clr-select-wrapper">
<select id="select-error" class="clr-select" name="storageUnit" [(ngModel)]="quotaHardLimitValue.storageUnit">
<ng-template ngFor let-quotaUnit [ngForOf]="quotaUnits" let-i="index">
<option *ngIf="i>1" [value]="quotaUnit.UNIT">{{ quotaUnit?.UNIT }}</option>
</ng-template>
</select>
</div>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.danger]="isDangerColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)"
[class.warning]="isWarningColor(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)" >
<progress value="{{storageInput.invalid || +quotaHardLimitValue.storageLimit === -1 ?0:quotaHardLimitValue.storageUsed}}"
max="{{storageInput.invalid?0:getByte(+quotaHardLimitValue.storageLimit, quotaHardLimitValue.storageUnit)}}"
data-displayval="100%"></progress>
</div>
<label class="progress-label">
<!-- the comments of progress , when storageLimit !=-1 get integet and unit in hard storage and used storage;and the unit of used storage <= the unit of hard storage
the other : get suitable number and unit-->
{{ +quotaHardLimitValue.storageLimit !== -1 ?(getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partNumberUsed
+ getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partCharacterUsed) : getSuitableUnit(quotaHardLimitValue.storageUsed)}}
{{ 'QUOTA.OF' | translate }}
{{ storageInput?.valid? +quotaHardLimitValue?.storageLimit ===-1? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.storageLimit :('QUOTA.INVALID_INPUT' | translate)}}
{{+quotaHardLimitValue?.storageLimit ===-1?'':quotaHardLimitValue?.storageUnit }}
</label>
</div>
</label>
</div>
</div>
<input clrInput name="storage" type="text" #storageInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.storageLimit"
id="storage" size="40"/>
<clr-control-error>{{ 'PROJECT.STORAGE_QUOTA_TIP' | translate }}</clr-control-error>
</clr-input-container>
</form>
</div>
<div class="modal-footer">

View File

@ -3,44 +3,61 @@
}
.modal-body {
outline: none;
padding-top: 0.8rem;
overflow-y: visible;
overflow-x: visible;
.clr-form-compact {
div.form-group {
padding-left: 8.5rem;
.mr-3px {
margin-right: 3px;
}
.quota-input {
width: 2rem;
padding-right: 0.8rem;
}
.select-div {
width: 2.5rem;
::ng-deep .clr-form-control {
margin-top: 0.28rem;
select {
padding-right: 15px;
}
}
}
.clr-form {
.left-label {
width: 9.5rem;
}
.mr-3px {
margin-right: 3px;
}
.left-label {
position: relative;
}
.quota-input {
width: 1.5rem;
margin-left: 1rem;
margin-right: 0.2rem;
}
.clr-validate-icon {
margin-left: -0.6rem;
}
.select-div {
width: 2.5rem;
}
.clr-select-wrapper {
position: absolute;
right: -5rem;
top: -0.08rem;
}
}
.clr-form-compact-common {
div.form-group {
padding-left: 6rem;
.left-label {
width: 7.5rem;
}
.select-div {
width: 1.6rem;
}
.quota-input {
margin-left: 0rem;
}
.select-div {
width: 1.6rem;
}
.clr-select-wrapper {
right: -4rem;
}
}
}
@ -50,35 +67,26 @@
}
.progress-div {
position: relative;
position: absolute;
padding-right: 0.6rem;
width: 9rem;
}
::ng-deep {
.progress {
&.warning>progress {
color: orange;
&::-webkit-progress-value {
background-color: orange;
}
&::-moz-progress-bar {
background-color: orange;
}
}
}
top: 0.2rem;
right: -13.1rem;
}
.progress-label {
position: absolute;
right: -2.3rem;
top: 0;
width: 3.5rem;
right: -3rem;
margin-top: 0;
width: 4rem;
font-weight: 100;
font-size: 10px;
}
overflow: hidden;
text-overflow: ellipsis;
::ng-deep {
.clr-error {
.clr-validate-icon {
margin-left: -12px;
}
}
}

View File

@ -38,10 +38,10 @@ export class EditProjectQuotasComponent implements OnInit {
staticBackdrop = true;
closable = false;
quotaForm: NgForm;
@ViewChild(InlineAlertComponent)
@ViewChild(InlineAlertComponent, {static: false})
inlineAlert: InlineAlertComponent;
@ViewChild('quotaForm')
@ViewChild('quotaForm', {static: true})
currentForm: NgForm;
@Output() confirmAction = new EventEmitter();
quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT;

View File

@ -40,3 +40,19 @@
margin-top: auto;
cursor: pointer;
}
::ng-deep {
.progress {
&.warning>progress {
color: orange;
&::-webkit-progress-value {
background-color: orange;
}
&::-moz-progress-bar {
background-color: orange;
}
}
}
}

View File

@ -33,7 +33,7 @@ const QuotaType = 'project';
export class ProjectQuotasComponent implements OnChanges {
config: Configuration = new Configuration();
@ViewChild('editProjectQuotas')
@ViewChild('editProjectQuotas', {static: false})
editQuotaDialog: EditProjectQuotasComponent;
loading = true;
quotaHardLimitValue: QuotaHardLimitInterface;

View File

@ -29,10 +29,10 @@ export class RegistryConfigComponent implements OnInit {
@Input() hasAdminRole: boolean = false;
@ViewChild("systemSettings") systemSettings: SystemSettingsComponent;
@ViewChild("vulnerabilityConfig") vulnerabilityCfg: VulnerabilityConfigComponent;
@ViewChild("gc") gc: GcComponent;
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
@ViewChild("systemSettings", {static: false}) systemSettings: SystemSettingsComponent;
@ViewChild("vulnerabilityConfig", {static: false}) vulnerabilityCfg: VulnerabilityConfigComponent;
@ViewChild("gc", {static: false}) gc: GcComponent;
@ViewChild("cfgConfirmationDialog", {static: false}) confirmationDlg: ConfirmationDialogComponent;
constructor(
private configService: ConfigurationService,

View File

@ -22,7 +22,7 @@ export class ReplicationConfigComponent {
@Input() showSubTitle: boolean = false;
@ViewChild("replicationConfigFrom") replicationConfigForm: NgForm;
@ViewChild("replicationConfigFrom", { static: false }) replicationConfigForm: NgForm;
get editable(): boolean {
return this.replicationConfig &&

View File

@ -1,140 +1,142 @@
<form #systemConfigFrom="ngForm" class="compact">
<section class="form-block">
<form #systemConfigFrom="ngForm" class="clr-form clr-form-horizontal">
<section>
<label class="subtitle" *ngIf="showSubTitle">{{'CONFIG.SYSTEM' | translate}}</label>
<div class="form-group">
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}</label>
<div class="select">
<select id="proCreation" name="proCreation"
[(ngModel)]="systemSettings.project_creation_restriction.value"
[disabled]="disabled(systemSettings.project_creation_restriction)">
<option value="everyone">{{'CONFIG.PRO_CREATION_EVERYONE' | translate }}</option>
<option value="adminonly">{{'CONFIG.PRO_CREATION_ADMIN' | translate }}</option>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.PRO_CREATION_RESTRICTION' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
<label for="tokenExpiration" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel"
[(ngModel)]="systemSettings.token_expiration.value"
required pattern="^[1-9]{1}[0-9]*$" id="tokenExpiration" size="20" [disabled]="!editable">
<span class="tooltip-content">
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
</span>
<clr-select-container>
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.TOOLTIP.PRO_CREATION_RESTRICTION' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="robotTokenExpiration" class="required">{{'ROBOT_ACCOUNT.TOKEN_EXPIRATION' | translate}}</label>
<label for="robotTokenExpiration" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-md tooltip-top-right"
[class.invalid]="robotTokenExpirationInput.invalid && (robotTokenExpirationInput.dirty || robotTokenExpirationInput.touched)">
<input name="robotTokenExpiration" type="text" #robotTokenExpirationInput="ngModel"
(ngModelChange)="changeToken($event)" [(ngModel)]="robotTokenExpiration"
required pattern="^[1-9]{1}[0-9]*$" id="robotTokenExpiration" size="20"
[disabled]="!robotExpirationEditable">
<span class="tooltip-content">
{{'ROBOT_ACCOUNT.NUMBER_REQUIRED' | translate}}
</span>
<select clrSelect id="proCreation" name="proCreation"
[(ngModel)]="systemSettings.project_creation_restriction.value"
[disabled]="disabled(systemSettings.project_creation_restriction)">
<option value="everyone">{{'CONFIG.PRO_CREATION_EVERYONE' | translate }}</option>
<option value="adminonly">{{'CONFIG.PRO_CREATION_ADMIN' | translate }}</option>
</select>
</clr-select-container>
<clr-input-container>
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.ROBOT_TOKEN_EXPIRATION' | translate}}</span>
</a>
</div>
<div class="form-group" *ngIf="canDownloadCert">
<label for="certDownloadLink" class="required">{{'CONFIG.ROOT_CERT' | translate}}</label>
<input clrInput name="tokenExpiration" type="text" #tokenExpirationInput="ngModel"
[(ngModel)]="systemSettings.token_expiration.value" required pattern="^[1-9]{1}[0-9]*$"
id="tokenExpiration" size="20" [disabled]="!editable" />
<clr-control-error>{{'TOOLTIP.NUMBER_REQUIRED' | translate}}</clr-control-error>
</clr-input-container>
<clr-input-container>
<label for="robotTokenExpiration" class="required">{{'ROBOT_ACCOUNT.TOKEN_EXPIRATION' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.TOOLTIP.ROBOT_TOKEN_EXPIRATION' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<input clrInput name="robotTokenExpiration" type="text" #robotTokenExpirationInput="ngModel"
(ngModelChange)="changeToken($event)" [(ngModel)]="robotTokenExpiration" required
pattern="^[1-9]{1}[0-9]*$" id="robotTokenExpiration" size="20" [disabled]="!robotExpirationEditable" />
<clr-control-error>{{'ROBOT_ACCOUNT.NUMBER_REQUIRED' | translate}}</clr-control-error>
</clr-input-container>
<label *ngIf="canDownloadCert" for="certDownloadLink"
class="required clr-control-label mt-1">{{'CONFIG.ROOT_CERT' | translate}}
<a #certDownloadLink [href]="downloadLink" target="_blank">{{'CONFIG.ROOT_CERT_LINK' | translate}}</a>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.ROOT_CERT_DOWNLOAD' | translate}}</span>
</a>
</div>
<div *ngIf="!withAdmiral" class="form-group">
<label for="repoReadOnly">{{'CONFIG.REPO_READ_ONLY' | translate}}</label>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.TOOLTIP.ROOT_CERT_DOWNLOAD' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<clr-checkbox-container *ngIf="!withAdmiral">
<label for="repoReadOnly">{{'CONFIG.REPO_READ_ONLY' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.TOOLTIP.REPO_TOOLTIP' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="repoReadOnly" id="repoReadOnly"
[ngModel]="systemSettings.read_only.value"
(ngModelChange)="setRepoReadOnlyValue($event)"/>
<label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-top-right read-tooltip">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.REPO_TOOLTIP' | translate}}</span>
</a>
</label>
[ngModel]="systemSettings.read_only.value" (ngModelChange)="setRepoReadOnlyValue($event)" />
</clr-checkbox-wrapper>
</div>
<div class="form-group" *ngIf="withClair">
<label for="systemWhitelist">{{'CVE_WHITELIST.DEPLOYMENT_SECURITY'|translate}}</label>
<div class="form-content w-100">
<div>
<div>
<span class="title">{{'CVE_WHITELIST.CVE_WHITELIST'|translate}}</span>
</clr-checkbox-container>
<div class="clr-form-control d-f" *ngIf="withClair">
<label for="systemWhitelist"
class="clr-control-label">{{'CVE_WHITELIST.DEPLOYMENT_SECURITY'|translate}}</label>
<div class="form-content">
<div class="font-size-13">
<div class="mt-05">
<span class="title font-size-13">{{'CVE_WHITELIST.CVE_WHITELIST'|translate}}</span>
</div>
<div>
<div class="mt-05">
<span>{{'CVE_WHITELIST.SYS_WHITELIST_EXPLAIN'|translate}}</span>
</div>
<div>
<div class="mt-05">
<span>{{'CVE_WHITELIST.ADD_SYS'|translate}}</span>
</div>
<div *ngIf="hasExpired">
<div class="mt-05" *ngIf="hasExpired">
<span class="label label-warning">{{'CVE_WHITELIST.WARNING_SYS'|translate}}</span>
</div>
</div>
<div class="clr-row width-70per">
<div class="clr-col position-relative">
<div class="clr-row width-90per">
<div class="position-relative pl-05">
<div>
<button id="show-add-modal-button" (click)="showAddModal=!showAddModal"
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
</div>
<div class="add-modal" *ngIf="showAddModal">
<clr-icon (click)="showAddModal=false" class="float-lg-right margin-top-4" shape="window-close"></clr-icon>
<clr-icon (click)="showAddModal=false" class="float-lg-right margin-top-4"
shape="window-close"></clr-icon>
<div>
<clr-textarea-container>
<clr-textarea-container class="flex-direction-column">
<label>{{'CVE_WHITELIST.ENTER'|translate}}</label>
<textarea id="whitelist-textarea" class="w-100 font-italic" clrTextarea [(ngModel)]="cveIds"
name="cveIds"></textarea>
name="cveIds"></textarea>
<clr-control-helper>{{'CVE_WHITELIST.HELP'|translate}}</clr-control-helper>
</clr-textarea-container>
</div>
<div>
<button id="add-to-system" [disabled]="isDisabled()" (click)="addToSystemWhitelist()"
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
class="btn btn-link">{{'CVE_WHITELIST.ADD'|translate}}</button>
</div>
</div>
<ul class="whitelist-window">
<li *ngIf="systemWhitelist?.items?.length<1"
class="none">{{'CVE_WHITELIST.NONE'|translate}}</li>
<li *ngIf="systemWhitelist?.items?.length<1" class="none">{{'CVE_WHITELIST.NONE'|translate}}
</li>
<li *ngFor="let item of systemWhitelist?.items;let i = index;">
<span class="hand" (click)="goToDetail(item.cve_id)">{{item.cve_id}}</span>
<clr-icon (click)="deleteItem(i)" class="float-lg-right margin-top-4"
shape="times-circle"></clr-icon>
shape="times-circle"></clr-icon>
</li>
</ul>
</div>
<div class="clr-col padding-top-8">
<div class="form-group padding-left-80">
<label for="expires">{{'CVE_WHITELIST.EXPIRES_AT'|translate}}</label>
<div class="underline">
<div class="clr-row expire-data">
<label class="bottom-line clr-col-4"
for="expires">{{'CVE_WHITELIST.EXPIRES_AT'|translate}}</label>
<div>
<input #dateInput placeholder="{{'CVE_WHITELIST.NEVER_EXPIRES'|translate}}" readonly
type="date" [(clrDate)]="expiresDate" newFormLayout="true">
type="date" [(clrDate)]="expiresDate" newFormLayout="true">
</div>
</div>
<div class="form-group padding-left-80">
<div class="clr-row">
<label class="clr-col-4"></label>
<clr-checkbox-wrapper>
<input [checked]="neverExpires" [(ngModel)]="neverExpires" type="checkbox" clrCheckbox
name="neverExpires" id="neverExpires"/>
name="neverExpires" id="neverExpires" />
<label>
{{'CVE_WHITELIST.NEVER_EXPIRES'|translate}}
</label>
@ -144,27 +146,30 @@
</div>
</div>
</div>
<div class="form-group">
<label for="webhookNotificationEnabled">{{'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate}}</label>
<clr-checkbox-container *ngIf="!withAdmiral">
<label for="webhookNotificationEnabled">{{'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
<span>{{'CONFIG.TOOLTIP.WEBHOOK_TOOLTIP' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox name="webhookNotificationEnabled" id="webhookNotificationEnabled" [ngModel]="systemSettings.notification_enable.value"
(ngModelChange)="setWebhookNotificationEnabledValue($event)" [ngModel]="systemSettings.notification_enable.value"/>
<label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right read-tooltip">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.WEBHOOK_TOOLTIP' | translate}}</span>
</a>
</label>
<input type="checkbox" clrCheckbox name="webhookNotificationEnabled" id="webhookNotificationEnabled"
[ngModel]="systemSettings.notification_enable.value"
(ngModelChange)="setWebhookNotificationEnabledValue($event)"
[ngModel]="systemSettings.notification_enable.value" />
</clr-checkbox-wrapper>
</div>
</clr-checkbox-container>
</section>
</form>
<div>
<button type="button" id="config_system_save" class="btn btn-primary" (click)="save()"
[disabled]="(!isValid() || !hasChanges()) && (!hasWhitelistChanged) || inProgress">{{'BUTTON.SAVE'
[disabled]="(!isValid() || !hasChanges()) && (!hasWhitelistChanged) || inProgress">{{'BUTTON.SAVE'
| translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()"
[disabled]="(!isValid() || !hasChanges()) && (!hasWhitelistChanged) || inProgress">{{'BUTTON.CANCEL'
[disabled]="(!isValid() || !hasChanges()) && (!hasWhitelistChanged) || inProgress">{{'BUTTON.CANCEL'
| translate}}</button>
</div>
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>

View File

@ -3,12 +3,43 @@
font-weight: 600;
}
.create-tooltip {
top: -1;
.clr-form-horizontal {
.clr-form-control {
& >.clr-control-label {
width: 10rem;
}
}
.flex-direction-column {
flex-direction: column;
}
}
.bottom-line {
display: flex;
flex-direction: column-reverse;
}
.expire-data {
min-width: 12.5rem;
margin-top: -1rem;
}
.position-relative {
position: relative;
}
.pl-05 {
padding-left: 0.5rem;
}
.mt-1 {
margin-top: 1rem;
}
.read-tooltip {
top: -7px;
.d-f {
display: flex;
}
.font-size-13 {
font-size: 13px;
}
.mt-05 {
margin-bottom: 0.5rem;
}
.title {
@ -34,18 +65,14 @@
}
}
.width-70per {
width: 70%;
.width-90per {
width: 90%;
}
.none {
color: #ccc;
}
.underline {
border-bottom: 1px solid;
}
.color-0079bb {
color: #0079bb;
}

View File

@ -66,9 +66,9 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
@Input() hasCAFile: boolean = false;
@Input() withAdmiral = false;
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
@ViewChild('dateInput') dateInput: ElementRef;
@ViewChild("systemConfigFrom", {static: false}) systemSettingsForm: NgForm;
@ViewChild("cfgConfirmationDialog", {static: false}) confirmationDlg: ConfirmationDialogComponent;
@ViewChild('dateInput', {static: false}) dateInput: ElementRef;
get editable(): boolean {
return this.systemSettings &&

View File

@ -34,7 +34,7 @@ export class VulnerabilityConfigComponent implements OnInit {
openState: boolean = false;
getLabelCurrent: string;
@ViewChild(CronScheduleComponent)
@ViewChild(CronScheduleComponent, {static: false})
CronScheduleComponent: CronScheduleComponent;
@Input()
@ -109,7 +109,7 @@ export class VulnerabilityConfigComponent implements OnInit {
};
}
}
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
@ViewChild("systemConfigFrom", {static: false}) systemSettingsForm: NgForm;
get isValid(): boolean {
return this.systemSettingsForm && this.systemSettingsForm.valid;

View File

@ -9,98 +9,96 @@
</span>
</div>
</div>
<form #targetForm="ngForm">
<section class="form-block">
<!-- provider -->
<div class="form-group">
<label class="form-group-label-override required">{{'DESTINATION.PROVIDER' | translate}}</label>
<div class="form-select">
<div class="select inputWidth pull-left">
<select name="adapter" id="adapter" (change)="adapterChange($event)" [(ngModel)]="target.type" [disabled]="testOngoing || editDisabled">
<option *ngFor="let adapter of adapterList" value="{{adapter}}">{{adapter}}</option>
</select>
</div>
<form #targetForm="ngForm" class="clr-form clr-form-horizontal">
<!-- provider -->
<clr-select-container>
<label class="required">{{'DESTINATION.PROVIDER' | translate}}</label>
<select clrSelect name="adapter" id="adapter" (change)="adapterChange($event)" [(ngModel)]="target.type" [disabled]="testOngoing || editDisabled">
<option *ngFor="let adapter of adapterList" value="{{adapter}}">{{adapter}}</option>
</select>
</clr-select-container>
<!-- Endpoint name -->
<clr-input-container>
<label class="required">{{ 'DESTINATION.NAME' | translate }}</label>
<input clrInput type="text" id="destination_name" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.name"
name="targetName" size="30" #targetName="ngModel" required>
<clr-control-error *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{ 'DESTINATION.NAME_IS_REQUIRED' | translate }}
</clr-control-error>
</clr-input-container>
<!--Description-->
<clr-textarea-container>
<label>{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea clrTextarea type="text" class="inputWidth" row=3 name="description" [(ngModel)]="target.description"></textarea>
</clr-textarea-container>
<!-- Endpoint Url -->
<div class="clr-form-control">
<label for="destination_url" class="required clr-control-label">{{ 'DESTINATION.URL' | translate }}</label>
<div class="clr-control-container" [class.clr-error]="targetEndpoint?.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
<div class="clr-input-wrapper" *ngIf="!endpointList.length">
<input class="clr-input" type="text" id="destination_url" [disabled]="testOngoing || urlDisabled" [readonly]="!editable"
[(ngModel)]="target.url" size="30" name="endpointUrl" #targetEndpoint="ngModel" required placeholder="http(s)://192.168.1.1">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
</div>
<!-- Endpoint name -->
<div class="form-group">
<label for="destination_name" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.NAME' |
translate }}</label>
<label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)"
[class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="destination_name" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.name"
name="targetName" size="25" #targetName="ngModel" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{ 'DESTINATION.NAME_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<!--Description-->
<div class="form-group">
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea type="text" class="inputWidth" row=3 name="description" [(ngModel)]="target.description"></textarea>
</div>
<!-- Endpoint Url -->
<div class="form-group">
<label for="destination_url" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.URL' |
translate }}</label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint?.errors && (targetEndpoint?.dirty || targetEndpoint?.touched)"
[class.valid]="targetEndpoint?.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left clr-select-wrapper">
<input *ngIf="!endpointList.length" type="text" id="destination_url" [disabled]="testOngoing || urlDisabled" [readonly]="!editable" [(ngModel)]="target.url"
size="25" name="endpointUrl" #targetEndpoint="ngModel" required placeholder="http(s)://192.168.1.1">
<select id="destination_url" *ngIf="endpointList.length" [(ngModel)]="target.url" class="clr-select" name="endpointUrl" #targetEndpoint="ngModel">
<div class="clr-select-wrapper" *ngIf="endpointList.length">
<select id="destination_url" class="clr-select" *ngIf="endpointList.length" [(ngModel)]="target.url"
name="endpointUrl" #targetEndpoint="ngModel">
<option class="display-none" value=""></option>
<option *ngFor="let endpoint of endpointList" value="{{endpoint.value}}">{{endpoint.key}}</option>
</select>
<span class="tooltip-content" *ngIf="targetEndpoint?.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<clr-control-error *ngIf="targetEndpoint?.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
</clr-control-error>
</div>
<!-- access_key -->
<div class="form-group">
<label for="destination_access_key" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_ID' |
translate }}</label>
<input type="text" placeholder="Access ID" class="col-md-8" id="destination_access_key" [disabled]="testOngoing" [readonly]="target.type ==='google-gcr' || !editable"
[(ngModel)]="target.credential.access_key" size="28" name="access_key" #access_key="ngModel">
</div>
<!-- access_key -->
<clr-input-container>
<label>{{ 'DESTINATION.ACCESS_ID' | translate }}</label>
<input clrInput type="text" placeholder="Access ID" id="destination_access_key" [disabled]="testOngoing" [readonly]="target.type ==='google-gcr' || !editable"
[(ngModel)]="target.credential.access_key" size="30" name="access_key" #access_key="ngModel">
</clr-input-container>
<!-- access_secret -->
<div class="clr-form-control">
<label for="destination_password" class="clr-control-label">{{ 'DESTINATION.ACCESS_SECRET' | translate }}</label>
<div class="clr-control-container">
<div class="clr-textarea-wrapper">
<input class="clr-input" *ngIf="target.type !=='google-gcr';else gcr_secret" type="password" placeholder="Access Secret"
id="destination_password" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.credential.access_secret"
size="30" name="access_secret" #access_secret="ngModel">
<ng-template #gcr_secret>
<textarea type="text" row="3" placeholder="Json Secret" class="clr-textarea" id="destination_password" [disabled]="testOngoing"
[readonly]="!editable" [(ngModel)]="target.credential.access_secret" name="access_secret" #access_secret="ngModel"></textarea>
</ng-template>
</div>
</div>
<!-- access_secret -->
<div class="form-group">
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_SECRET' |
translate }}</label>
<input *ngIf="target.type !=='google-gcr';else gcr_secret" type="password" placeholder="Access Secret" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
[(ngModel)]="target.credential.access_secret" size="28" name="access_secret" #access_secret="ngModel">
<ng-template #gcr_secret>
<textarea type="text" row="3" placeholder="Json Secret" class="inputWidth" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
[(ngModel)]="target.credential.access_secret" name="access_secret" #access_secret="ngModel"></textarea>
</ng-template>
</div>
<!-- Verify Remote Cert -->
<div class="form-group">
<label for="destination_insecure" id="destination_insecure_checkbox">{{'CONFIG.VERIFY_REMOTE_CERT' |
translate }}</label>
<input type="checkbox" clrCheckbox #insecure id="destination_insecure" [disabled]="testOngoing || !editable"
name="insecure" [ngModel]="!target.insecure" (ngModelChange)="setInsecureValue($event)" class="clr-checkbox">
</div>
<!-- Verify Remote Cert -->
<clr-checkbox-container>
<label id="destination_insecure_checkbox"
for="destination_insecure">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-right" clrSize="md" *clrIfOpen>
{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate}}
</clr-tooltip-content>
</clr-tooltip>
</div>
<div class="form-group" class="form-height">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!inProgress"></span>
</div>
</section>
</label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox #insecure id="destination_insecure" [disabled]="testOngoing || !editable" name="insecure"
[ngModel]="!target.insecure" (ngModelChange)="setInsecureValue($event)">
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="clr-form-control" class="form-height">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!inProgress"></span>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="inProgress || (targetEndpoint?.errors)">{{
'DESTINATION.TEST_CONNECTION' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' |
translate }}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit()" [disabled]="!isValid">{{ 'BUTTON.OK' | translate
}}</button>
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="inProgress || (targetEndpoint?.errors)">{{ 'DESTINATION.TEST_CONNECTION' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit()" [disabled]="!isValid">{{ 'BUTTON.OK' | translate }}
</button>
</div>
</clr-modal>

View File

@ -63,13 +63,13 @@ export class CreateEditEndpointComponent
selectedType: string;
initVal: Endpoint;
targetForm: NgForm;
@ViewChild("targetForm") currentForm: NgForm;
@ViewChild("targetForm", {static: false}) currentForm: NgForm;
targetEndpoint;
testOngoing: boolean;
onGoing: boolean;
endpointId: number | string;
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
@ViewChild(InlineAlertComponent, {static: false}) inlineAlert: InlineAlertComponent;
@Output() reload = new EventEmitter<boolean>();

View File

@ -3,13 +3,13 @@
<section>
<label>
<label for="name">{{'LABEL.LABEL_NAME' | translate}}</label>
<label aria-haspopup="true" role="tooltip" [class.invalid]="isLabelNameExist"
class="tooltip tooltip-validation tooltip-md tooltip-bottom-left">
<input type="text" id="name" name="name" required size="20" autocomplete="off"
<label class="clr-control-container" [class.clr-error]="isLabelNameExist">
<input clrInput type="text" id="name" name="name" required size="20" autocomplete="off"
[(ngModel)]="labelModel.name" #name="ngModel" (keyup)="existValid(labelModel.name)">
<span class="tooltip-content">
{{'LABEL.NAME_ALREADY_EXISTS' | translate }}
</span>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<clr-control-error class="position-ab" *ngIf="isLabelNameExist">
{{'LABEL.NAME_ALREADY_EXISTS' | translate }}
</clr-control-error>
</label>
</label>
<label>
@ -21,11 +21,11 @@
[class.borderSty]="i.color == '#FFFFFF'" [ngStyle]="{'background-color': i.color, 'color': i.textColor }">Aa</label>
</clr-dropdown-menu>
</clr-dropdown>
<input type="text" id="color" size="8" name="color" disabled [(ngModel)]="labelModel.color" #color="ngModel">
<input clrInput type="text" id="color" size="8" name="color" disabled [(ngModel)]="labelModel.color" #color="ngModel">
</label>
<label>
<label for="description">{{'LABEL.DESCRIPTION' | translate}}</label>
<input type="text" id="description" name="description" size="30" [(ngModel)]="labelModel.description"
<input clrInput type="text" id="description" name="description" size="30" [(ngModel)]="labelModel.description"
#description="ngModel">
</label>
<label>

View File

@ -58,8 +58,15 @@ form {
.borderSty
{
border: 1px solid #A1A1A1 !important;
border: 1px solid #A1A1A1 !important;
line-height: 22px;
}
}
::ng-deep {
.clr-form-control {
display: inherit;
}
}
.position-ab {
position: absolute;
}

View File

@ -58,7 +58,7 @@ describe("CreateEditLabelComponent (inline template)", () => {
labelService = fixture.debugElement.injector.get(LabelService);
spy = spyOn(labelService, "getLabels").and.returnValue(
of(mockOneData)
of([mockOneData])
);
spyOne = spyOn(labelService, "createLabel").and.returnValue(
of(mockOneData)

View File

@ -52,7 +52,7 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
nameChecker = new Subject<string>();
labelForm: NgForm;
@ViewChild("labelForm") currentForm: NgForm;
@ViewChild("labelForm", {static: true}) currentForm: NgForm;
@Input() projectId: number;
@Input() scope: string;
@ -71,7 +71,7 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
this.isLabelNameExist = false;
if (targets && targets.length) {
if (targets.find((target) => {
return target.name === name;
return target.name === name && target.id !== this.labelId;
})) {
this.isLabelNameExist = true;
}

View File

@ -2,212 +2,223 @@
<h3 class="modal-title">{{headerTitle | translate}}</h3>
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
<div class="modal-body modal-body-height">
<form [formGroup]="ruleForm" novalidate>
<section class="form-block">
<div class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.NAME' | translate}}</label>
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || !isRuleNameValid'>
<input type="text" id="ruleName" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth" required maxlength="255" formControlName="name"
#ruleName (keyup)='checkRuleName()' autocomplete="off">
<span class="tooltip-content">{{ruleNameTooltip | translate}}</span>
<form [formGroup]="ruleForm" novalidate class="clr-form clr-form-horizontal">
<div class="clr-form-control" [class.clr-error]="(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || !isRuleNameValid">
<label class="clr-control-label required">{{'REPLICATION.NAME' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input class="clr-input" type="text" id="ruleName" size="35" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" required maxlength="255"
formControlName="name" #ruleName (keyup)='checkRuleName()' autocomplete="off">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline spinner-pos" [hidden]="!inNameChecking"></span>
</div>
<clr-control-error *ngIf="(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || !isRuleNameValid">{{ruleNameTooltip | translate}}</clr-control-error>
</div>
</div>
<!--Description-->
<clr-textarea-container>
<label>{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea clrTextarea type="text" id="ruleDescription" class="inputWidth" row=3 formControlName="description"></textarea>
</clr-textarea-container>
<!-- replication mode -->
<clr-radio-container clrInline>
<label>{{'REPLICATION.REPLI_MODE' | translate}}</label>
<clr-radio-wrapper>
<input clrRadio type="radio" id="push_base" name="replicationMode" [value]=true [disabled]="policyId >= 0 || onGoing" [(ngModel)]="isPushMode"
(change)="pushModeChange()" [ngModelOptions]="{standalone: true}">
<label for="push_base">Push-based
<clr-tooltip class="mode-tooltip">
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span>{{'TOOLTIP.PUSH_BASED' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<span class="spinner spinner-inline spinner-pos" [hidden]="!inNameChecking"></span>
</div>
<!--Description-->
<div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea type="text" id="ruleDescription" class="inputWidth" row=3 formControlName="description"></textarea>
</div>
<!-- replication mode -->
<div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.REPLI_MODE' | translate}}</label>
<div class="radio-inline" [class.disabled]="policyId >= 0 || onGoing">
<input type="radio" id="push_base" name="replicationMode" [value]=true [disabled]="policyId >= 0 || onGoing" [(ngModel)]="isPushMode" (change)="pushModeChange()" [ngModelOptions]="{standalone: true}">
<label for="push_base">Push-based</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" id="pull_base" name="replicationMode" [value]=false [disabled]="policyId >= 0 || onGoing" [(ngModel)]="isPushMode"
(change)="pullModeChange()" [ngModelOptions]="{standalone: true}">
<label for="pull_base">Pull-based
<clr-tooltip class="mode-tooltip">
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span>{{'TOOLTIP.PUSH_BASED' | translate}}</span>
<span>{{'TOOLTIP.PULL_BASED' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
</clr-radio-wrapper>
</clr-radio-container>
<!--source registry-->
<div class="clr-form-control" *ngIf="!isPushMode">
<label class="required clr-control-label">{{'REPLICATION.SOURCE_REGISTRY' | translate}}</label>
<div class="clr-control-container">
<div class="clr-select-wrapper">
<select class="clr-select select-width" id="src_registry_id" (change)="sourceChange($event)" formControlName="src_registry"
[compareWith]="equals">
<option class="display-none"></option>
<option *ngFor="let source of sourceList" [ngValue]="source">{{source.name}}-{{source.url}}</option>
</select>
</div>
<div class="radio-inline" [class.disabled]="policyId >= 0 || onGoing">
<input type="radio" id="pull_base" name="replicationMode" [value]=false [disabled]="policyId >= 0 || onGoing" [(ngModel)]="isPushMode" (change)="pullModeChange()" [ngModelOptions]="{standalone: true}">
<label for="pull_base">Pull-based</label>
<clr-tooltip class="mode-tooltip">
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span>{{'TOOLTIP.PULL_BASED' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
<div class="space-between">
<span *ngIf="noEndpointInfo.length != 0" class="alert-label">{{noEndpointInfo | translate}}</span>
<span class="alert-label go-link" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
</div>
</div>
<!--source registry-->
<div *ngIf="!isPushMode" class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.SOURCE_REGISTRY' | translate}}</label>
<div class="form-select">
<div class="select endpointSelect pull-left">
<select id="src_registry_id" (change)="sourceChange($event)" formControlName="src_registry" [compareWith]="equals">
<option class="display-none"></option>
<option *ngFor="let source of sourceList" [ngValue]="source">{{source.name}}-{{source.url}}</option>
</select>
</div>
</div>
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
</div>
<!--images/Filter-->
<div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.SOURCE_RESOURCE_FILTER' | translate}}</label>
<span class="spinner spinner-inline spinner-position" [hidden]="onGoing === false"></span>
<div formArrayName="filters">
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
<div [formGroupName]="i" *ngIf="supportedFilters[i]?.type !=='label' || (supportedFilters[i]?.type==='label' && supportedFilterLabels?.length)">
<div class="width-70" >
<label>{{"REPLICATION." + supportedFilters[i]?.type.toUpperCase() | translate}}:</label>
</div>
<label *ngIf="supportedFilters[i]?.style==='input'" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
[class.invalid]='(filter.value.dirty || filter.value.touched) && filter.value.invalid'>
<input (input)="trimText($event)" type="text" #filterValue required size="14" formControlName="value" id="{{'filter_'+ supportedFilters[i]?.type}}">
<span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
</label>
<div class="select resource-box" *ngIf="supportedFilters[i]?.style==='radio' && supportedFilters[i]?.values.length > 1">
<select formControlName="value" #selectedValue id="{{'select_'+ supportedFilters[i]?.type}}" name="{{supportedFilters[i]?.type}}">
<option value="">{{'REPLICATION.BOTH' | translate}}</option>
<option *ngFor="let value of supportedFilters[i]?.values;" value="{{value}}">{{value}}</option>
</select>
</div>
<div class="select resource-box" *ngIf="supportedFilters[i]?.type==='label'&& supportedFilters[i]?.style==='list'">
<div class="dropdown width-100" formArrayName="value">
<clr-dropdown class="width-100">
<button type="button" class="width-100 dropdown-toggle btn btn-link statistic-data label-text" clrDropdownTrigger>
<ng-template ngFor let-label [ngForOf]="filter.value.value" let-m="index">
<span class="label" *ngIf="m<1"> {{label}} </span>
</ng-template>
<span class="ellipsis" *ngIf="filter.value.value.length>1">···</span>
<div *ngFor="let label1 of filter.value.value;let k = index" hidden="true">
<input type="text" [formControlName]="k" #labelValue id="{{'label_'+ supportedFilters[i]?.type + '_' + label1}}" name="{{'label_'+ supportedFilters[i]?.type + '_' + label1}}" placeholder="select labels" >
</div>
<!--images/Filter-->
<div class="clr-form-control">
<label class="clr-control-label">{{'REPLICATION.SOURCE_RESOURCE_FILTER' | translate}}</label>
<span class="spinner spinner-inline spinner-position" [hidden]="onGoing === false"></span>
<div formArrayName="filters" class="clr-control-container">
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
<div [formGroupName]="i" *ngIf="supportedFilters[i]?.type !=='label' || (supportedFilters[i]?.type==='label' && supportedFilterLabels?.length)">
<div class="width-70">
<label>{{"REPLICATION." + supportedFilters[i]?.type.toUpperCase() | translate}}:</label>
</div>
<label *ngIf="supportedFilters[i]?.style==='input'" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
[class.invalid]='(filter.value.dirty || filter.value.touched) && filter.value.invalid'>
<input class="clr-input" (input)="trimText($event)" type="text" #filterValue size="14" formControlName="value" id="{{'filter_'+ supportedFilters[i]?.type}}">
</label>
<div class="select resource-box clr-select-wrapper" *ngIf="supportedFilters[i]?.style==='radio' && supportedFilters[i]?.values.length > 1">
<select class="clr-select width-100" formControlName="value" #selectedValue id="{{'select_'+ supportedFilters[i]?.type}}"
name="{{supportedFilters[i]?.type}}">
<option value="">{{'REPLICATION.BOTH' | translate}}</option>
<option *ngFor="let value of supportedFilters[i]?.values;" value="{{value}}">{{value}}</option>
</select>
</div>
<div class="select resource-box" *ngIf="supportedFilters[i]?.type==='label'&& supportedFilters[i]?.style==='list'">
<div class="dropdown width-100 clr-select-wrapper" formArrayName="value">
<clr-dropdown class="width-100">
<button type="button" class="width-100 dropdown-toggle btn btn-link statistic-data label-text" clrDropdownTrigger>
<ng-template ngFor let-label [ngForOf]="filter.value.value" let-m="index">
<span class="label" *ngIf="m<1"> {{label}} </span>
</ng-template>
<span class="ellipsis" *ngIf="filter.value.value.length>1">···</span>
<div *ngFor="let label1 of filter.value.value;let k = index" hidden="true">
<input type="text" [formControlName]="k" #labelValue id="{{'label_'+ supportedFilters[i]?.type + '_' + label1}}" name="{{'label_'+ supportedFilters[i]?.type + '_' + label1}}"
placeholder="select labels">
</div>
</button>
<clr-dropdown-menu class="width-100" clrPosition="bottom-left" *clrIfOpen>
<button type="button" class="dropdown-item" *ngFor="let value of supportedFilterLabels" (click)="stickLabel(value,i)">
<clr-icon shape="check" [hidden]="!value.select" class='pull-left'></clr-icon>
<div class='labelDiv'>
<hbr-label-piece [label]="value" [labelWidth]="130"></hbr-label-piece>
</div>
</button>
<clr-dropdown-menu class="width-100" clrPosition="bottom-left" *clrIfOpen>
<button type="button" class="dropdown-item" *ngFor="let value of supportedFilterLabels" (click)="stickLabel(value,i)">
<clr-icon shape="check" [hidden]="!value.select" class='pull-left'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="value" [labelWidth]="130"></hbr-label-piece></div>
</button>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<div class="resource-box" *ngIf="supportedFilters[i]?.style==='radio' && supportedFilters[i]?.values.length <= 1">
<span>{{supportedFilters[i]?.values}}</span>
</div>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='name'">{{'TOOLTIP.NAME_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='tag'">{{'TOOLTIP.TAG_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='label'">{{'TOOLTIP.LABEL_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='resource'">{{'TOOLTIP.RESOURCE_FILTER' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
<div class="resource-box" *ngIf="supportedFilters[i]?.style==='radio' && supportedFilters[i]?.values.length <= 1">
<span>{{supportedFilters[i]?.values}}</span>
</div>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='name'">{{'TOOLTIP.NAME_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='tag'">{{'TOOLTIP.TAG_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='label'">{{'TOOLTIP.LABEL_FILTER' | translate}}</span>
<span class="tooltip-content" *ngIf="supportedFilters[i]?.type==='resource'">{{'TOOLTIP.RESOURCE_FILTER' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
</div>
</div>
<!--destination registry-->
<div *ngIf="isPushMode" class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.DEST_REGISTRY' | translate}}</label>
<div class="form-select">
<div class="select endpointSelect pull-left">
<select id="dest_registry" (change)="targetChange($event)" formControlName="dest_registry" [compareWith]="equals">
<option class="display-none"></option>
<option *ngFor="let target of targetList" [ngValue]="target">{{target.name}}-{{target.url}}</option>
</select>
</div>
</div>
<!--destination registry-->
<div *ngIf="isPushMode" class="clr-form-control">
<label class="clr-control-label required">{{'REPLICATION.DEST_REGISTRY' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span>{{'TOOLTIP.DESTINATION_NAMESPACE' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<div class="form-select clr-control-container">
<div class="clr-select-wrapper">
<select class="clr-select select-width" id="dest_registry" (change)="targetChange($event)" formControlName="dest_registry"
[compareWith]="equals">
<option class="display-none"></option>
<option *ngFor="let target of targetList" [ngValue]="target">{{target.name}}-{{target.url}}</option>
</select>
</div>
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
</div>
<!--destination namespaces -->
<div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.DEST_NAMESPACE' | translate}}</label>
<div class="form-select">
<div class="endpointSelect pull-left">
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='ruleForm.controls.dest_namespace.touched && ruleForm.controls.dest_namespace.invalid'>
<input formControlName="dest_namespace" type="text" id="dest_namespace" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth"
maxlength="255">
<span class="tooltip-content">{{'REPLICATION.DESTINATION_NAME_TOOLTIP' | translate}}</span>
</label>
</div>
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span>{{'TOOLTIP.DESTINATION_NAMESPACE' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
<div class="space-between">
<label *ngIf="noEndpointInfo.length != 0" class="alert-label">{{noEndpointInfo | translate}}</label>
<span class="alert-label go-link" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
</div>
</div>
<!--Trigger-->
<div class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.TRIGGER_MODE' | translate}}</label>
</div>
<!--destination namespaces -->
<clr-input-container>
<label>{{'REPLICATION.DEST_NAMESPACE' | translate}}
<clr-tooltip>
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span>{{'TOOLTIP.DESTINATION_NAMESPACE' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<input clrInput formControlName="dest_namespace" type="text" id="dest_namespace" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$"
class="inputWidth" maxlength="255">
<clr-control-error *ngIf='ruleForm.controls.dest_namespace.touched && ruleForm.controls.dest_namespace.invalid'>{{'REPLICATION.DESTINATION_NAME_TOOLTIP' | translate}}</clr-control-error>
</clr-input-container>
<!--Trigger-->
<div class="clr-form-control">
<label class="clr-control-label required">{{'REPLICATION.TRIGGER_MODE' | translate}}</label>
<div class="clr-control-container">
<div formGroupName="trigger">
<!--on trigger-->
<div class="select width-115">
<select id="ruleTrigger" formControlName="type">
<div class="select width-115 clr-select-wrapper">
<select id="ruleTrigger" formControlName="type" class="clr-select">
<option *ngFor="let trigger of supportedTriggers" [value]="trigger">{{'REPLICATION.' + trigger.toUpperCase() | translate }}</option>
</select>
</div>
<!--on push-->
<div formGroupName="trigger_settings">
<div [hidden]="isNotSchedule()" class="form-group form-cron">
<div formGroupName="trigger_settings" class="clr-form-control">
<div [hidden]="isNotSchedule()">
<label class="required">Cron String</label>
<label for="targetCron" aria-haspopup="true" role="tooltip"class="tooltip tooltip-validation tooltip-sm tooltip-top-right"
[class.invalid]="!isNotSchedule() && cronTouched && !cronInputValid(ruleForm.value.trigger?.trigger_settings?.cron || '')" >
<input type="text" name=targetCron id="targetCron" required class="form-control cron-input" formControlName="cron">
<label for="targetCron" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-sm tooltip-top-right"
[class.invalid]="!isNotSchedule() && cronTouched && !cronInputValid(ruleForm.value.trigger?.trigger_settings?.cron || '')">
<input type="text" name=targetCron id="targetCron" required class="form-control cron-input clr-input" formControlName="cron">
<span class="tooltip-content">
{{'TOOLTIP.CRON_REQUIRED' | translate }}
</span>
</label>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-left top-7 cron-tooltip">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<div class="tooltip-content table-box">
<cron-tooltip></cron-tooltip>
</div>
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<div class="tooltip-content table-box">
<cron-tooltip></cron-tooltip>
</div>
</a>
</div>
</div>
</div>
<div [hidden]="isNotEventBased()" class="clr-form-control rule-width">
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [checked]="false" id="ruleDeletion" formControlName="deletion" class="clr-checkbox">
<label for="ruleDeletion" class="clr-control-label">{{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}}</label>
</clr-checkbox-wrapper>
<div class="clr-checkbox-wrapper clr-form-control" [hidden]="isNotEventBased()">
<input type="checkbox" class="clr-checkbox" [checked]="false" id="ruleDeletion" formControlName="deletion">
<label for="ruleDeletion">{{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}}</label>
</div>
<div class="clr-form-control rule-width override-box">
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [checked]="true" id="overridePolicy" formControlName="override" class="clr-checkbox">
<label for="overridePolicy" class="clr-control-label">{{'REPLICATION.OVERRIDE_INFO' | translate}}</label>
</clr-checkbox-wrapper>
<div class="clr-checkbox-wrapper clr-form-control">
<input type="checkbox" class="clr-checkbox" [checked]="true" id="overridePolicy" formControlName="override">
<label for="overridePolicy">{{'REPLICATION.OVERRIDE_INFO' | translate}}
<clr-tooltip class="override-tooltip">
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
<clr-tooltip-content clrPosition="top-left" clrSize="md" *clrIfOpen>
<span>{{'TOOLTIP.OVERRIDE' | translate}}</span>
<span>{{'TOOLTIP.OVERRIDE' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
</div>
<div class="clr-checkbox-wrapper clr-form-control">
<input type="checkbox" [checked]="true" id="enablePolicy" formControlName="enabled" class="clr-checkbox">
<label for="enablePolicy" class="clr-control-label">{{'REPLICATION.ENABLED_RULE' | translate}}</label>
</div>
<div class="clr-form-control rule-width">
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [checked]="true" id="enablePolicy" formControlName="enabled" class="clr-checkbox">
<label for="enablePolicy" class="clr-control-label">{{'REPLICATION.ENABLED_RULE' | translate}}</label>
</clr-checkbox-wrapper>
</div>
</div>
<div class="loading-center">
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</div>
</section>
</form>
</div>
<div class="loading-center">
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="this.inProgress" (click)="onCancel()">{{ 'BUTTON.CANCEL' | translate }}</button>

View File

@ -14,19 +14,8 @@ h4 {
color: #666;
}
.colorRed {
color: red;
}
.colorRed a {
text-decoration: underline;
color: #007CBB;
}
.alertLabel {
display: block;
margin-top: 2px;
line-height: 1em;
.alert-label {
color:red;
font-size: 12px;
}
@ -42,6 +31,7 @@ h4 {
.filterSelect {
position: relative;
width: 315px;
margin-bottom:0.5rem;
}
.filterSelect clr-icon {
@ -86,10 +76,6 @@ h4 {
margin-right: 4px;
}
.form-group {
min-height: 36px;
}
.projectInput {
float: left;
position: relative;
@ -167,10 +153,10 @@ h4 {
margin-right: 120px;
}
.goLink {
.go-link {
color: blue;
border-bottom: 1px solid blue;
line-height: 14px;
line-height: 1rem;
cursor: pointer;
}
@ -293,4 +279,13 @@ clr-modal {
margin-left: 0.2rem;
font-size: 16px;
font-weight: 700;
}
.space-between {
display: flex;
justify-content: space-between;
}
.select-width {
min-width:11rem;
}

View File

@ -75,7 +75,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
@Output() goToRegistry = new EventEmitter<any>();
@Output() reload = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
@ViewChild(InlineAlertComponent, {static: true}) inlineAlert: InlineAlertComponent;
constructor(
private fb: FormBuilder,
private repService: ReplicationService,

View File

@ -23,8 +23,8 @@
</div>
<div class="setting-wrapper flex-layout" *ngIf="isEditMode">
<span class="font-style">{{ labelEdit | translate }}</span>
<div class="select select-schedule">
<select name="selectPolicy" id="selectPolicy" [(ngModel)]="scheduleType">
<div class="select select-schedule clr-select-wrapper">
<select name="selectPolicy" id="selectPolicy" [(ngModel)]="scheduleType">
<option [value]="SCHEDULE_TYPE.NONE">{{'SCHEDULE.NONE' | translate}}</option>
<option [value]="SCHEDULE_TYPE.HOURLY">{{'SCHEDULE.HOURLY' | translate}}</option>
<option [value]="SCHEDULE_TYPE.DAILY">{{'SCHEDULE.DAILY' | translate}}</option>
@ -35,7 +35,7 @@
<span class="required" [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }}</span>
<div [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM" class="cron-input">
<label for="targetCron" aria-haspopup="true" role="tooltip" [class.invalid]="dateInvalid" class="tooltip tooltip-validation tooltip-md tooltip-top-left cron-label">
<input type="text" (blur)="blurInvalid()" (input)="inputInvalid()" name=targetCron id="targetCron" #cronStringInput="ngModel" required class="form-control"
<input type="text" (blur)="blurInvalid()" (input)="inputInvalid()" name=targetCron id="targetCron" #cronStringInput="ngModel" required class="clr-input form-control"
[(ngModel)]="cronString">
<span class="tooltip-content" *ngIf="dateInvalid">
{{'TOOLTIP.CRON_REQUIRED' | translate }}

View File

@ -17,7 +17,7 @@ export class DatePickerComponent implements OnChanges {
@Input() dateInput: string;
@Input() oneDayOffset: boolean;
@ViewChild("searchTime") searchTime: NgModel;
@ViewChild("searchTime", {static: true}) searchTime: NgModel;
@Output() search = new EventEmitter<string>();

View File

@ -142,7 +142,7 @@ describe("EndpointComponent (inline template)", () => {
spyOnRules = spyOn(
endpointService,
"getEndpointWithReplicationRules"
).and.returnValue([]);
).and.returnValue(of([]));
spyOne = spyOn(endpointService, "getEndpoint").and.returnValue(
of(mockOne[0])
);

View File

@ -53,10 +53,10 @@ import { OperationService } from "../operation/operation.service";
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EndpointComponent implements OnInit, OnDestroy {
@ViewChild(CreateEditEndpointComponent)
@ViewChild(CreateEditEndpointComponent, {static: false})
createEditEndpointComponent: CreateEditEndpointComponent;
@ViewChild("confirmationDialog")
@ViewChild("confirmationDialog", {static: false})
confirmationDialogComponent: ConfirmationDialogComponent;
targets: Endpoint[];

Some files were not shown because too many files have changed in this diff Show More