mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-03 14:37:44 +01:00
Merge branch 'master' of https://github.com/goharbor/harbor into immutable-selector
This commit is contained in:
commit
cab07f71cd
@ -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:
|
||||
|
@ -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 | | | | |
|
||||
|
||||
|
34
make/migrations/postgresql/0011_1.10.0_schema.up.sql
Normal file
34
make/migrations/postgresql/0011_1.10.0_schema.up.sql
Normal 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)
|
||||
)
|
@ -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
|
||||
|
@ -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)
|
||||
|
348
src/core/api/plug_scanners.go
Normal file
348
src/core/api/plug_scanners.go
Normal 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
|
||||
}
|
444
src/core/api/plug_scanners_test.go
Normal file
444
src/core/api/plug_scanners_test.go
Normal 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)
|
||||
}
|
@ -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"))
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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{})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
|
@ -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{}
|
||||
|
@ -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"
|
@ -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 {
|
@ -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 {
|
@ -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,
|
@ -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 {
|
@ -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")
|
||||
}
|
@ -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, ",")...)
|
@ -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
25
src/pkg/q/query.go
Normal 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
|
||||
}
|
@ -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:
|
||||
|
@ -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)
|
||||
*/
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)},
|
||||
|
@ -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 {
|
||||
|
@ -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)},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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()},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
157
src/pkg/scan/scanner/api/controller.go
Normal file
157
src/pkg/scan/scanner/api/controller.go
Normal 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)
|
||||
}
|
287
src/pkg/scan/scanner/api/controller_test.go
Normal file
287
src/pkg/scan/scanner/api/controller_test.go
Normal 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)
|
||||
}
|
194
src/pkg/scan/scanner/api/registration.go
Normal file
194
src/pkg/scan/scanner/api/registration.go
Normal 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
|
||||
}
|
35
src/pkg/scan/scanner/api/scan.go
Normal file
35
src/pkg/scan/scanner/api/scan.go
Normal 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
|
||||
}
|
43
src/pkg/scan/scanner/dao/scan/report.go
Normal file
43
src/pkg/scan/scanner/dao/scan/report.go
Normal 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"},
|
||||
}
|
||||
}
|
120
src/pkg/scan/scanner/dao/scanner/model.go
Normal file
120
src/pkg/scan/scanner/dao/scanner/model.go
Normal 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
|
||||
}
|
87
src/pkg/scan/scanner/dao/scanner/model_test.go
Normal file
87
src/pkg/scan/scanner/dao/scanner/model_test.go
Normal 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)
|
||||
}
|
147
src/pkg/scan/scanner/dao/scanner/registration.go
Normal file
147
src/pkg/scan/scanner/dao/scanner/registration.go
Normal 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
|
||||
}
|
144
src/pkg/scan/scanner/dao/scanner/registration_test.go
Normal file
144
src/pkg/scan/scanner/dao/scanner/registration_test.go
Normal 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)
|
||||
}
|
131
src/pkg/scan/scanner/manager.go
Normal file
131
src/pkg/scan/scanner/manager.go
Normal 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()
|
||||
}
|
115
src/pkg/scan/scanner/manager_test.go
Normal file
115
src/pkg/scan/scanner/manager_test.go
Normal 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)
|
||||
}
|
48
src/pkg/scan/scanner/scan/controller.go
Normal file
48
src/pkg/scan/scanner/scan/controller.go
Normal 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)
|
||||
}
|
46
src/pkg/scan/scanner/scan/models.go
Normal file
46
src/pkg/scan/scanner/scan/models.go
Normal 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
|
||||
}
|
@ -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": [
|
||||
|
@ -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",
|
||||
|
@ -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
5
src/portal/lib/package-lock.json
generated
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@harbor/ui",
|
||||
"version": "1.10.0",
|
||||
"lockfileVersion": 1
|
||||
}
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -4,26 +4,17 @@
|
||||
<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>
|
||||
|
||||
<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)">
|
||||
@ -34,31 +25,29 @@
|
||||
{{ countInput?.valid?+quotaHardLimitValue?.countLimit===-1 ? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.countLimit:('QUOTA.INVALID_INPUT' | translate)}}
|
||||
</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">
|
||||
<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>
|
||||
<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)" >
|
||||
@ -76,7 +65,12 @@
|
||||
{{+quotaHardLimitValue?.storageLimit ===-1?'':quotaHardLimitValue?.storageUnit }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<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">
|
||||
|
@ -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;
|
||||
.clr-form {
|
||||
.left-label {
|
||||
width: 9.5rem;
|
||||
}
|
||||
|
||||
.mr-3px {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.left-label {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quota-input {
|
||||
width: 2rem;
|
||||
padding-right: 0.8rem;
|
||||
width: 1.5rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
.clr-validate-icon {
|
||||
margin-left: -0.6rem;
|
||||
}
|
||||
|
||||
.select-div {
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
::ng-deep .clr-form-control {
|
||||
margin-top: 0.28rem;
|
||||
.clr-select-wrapper {
|
||||
position: absolute;
|
||||
right: -5rem;
|
||||
top: -0.08rem;
|
||||
}
|
||||
|
||||
select {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clr-form-compact-common {
|
||||
div.form-group {
|
||||
padding-left: 6rem;
|
||||
.left-label {
|
||||
width: 7.5rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 &&
|
||||
|
@ -1,107 +1,107 @@
|
||||
<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"
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<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.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>
|
||||
<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>
|
||||
<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="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>
|
||||
</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 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>
|
||||
</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>
|
||||
</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>
|
||||
@ -114,8 +114,8 @@
|
||||
</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"
|
||||
@ -124,14 +124,16 @@
|
||||
</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">
|
||||
</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" />
|
||||
@ -144,19 +146,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webhookNotificationEnabled">{{'CONFIG.WEBHOOK_NOTIFICATION_ENABLED' | translate}}</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>
|
||||
<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" />
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</clr-checkbox-container>
|
||||
</section>
|
||||
</form>
|
||||
<div>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 &&
|
||||
|
@ -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;
|
||||
|
@ -9,98 +9,96 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form #targetForm="ngForm">
|
||||
<section class="form-block">
|
||||
<form #targetForm="ngForm" class="clr-form clr-form-horizontal">
|
||||
<!-- 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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</clr-select-container>
|
||||
<!-- 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)">
|
||||
<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 }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</clr-control-error>
|
||||
</clr-input-container>
|
||||
<!--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>
|
||||
<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="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-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 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)">
|
||||
</div>
|
||||
<clr-control-error *ngIf="targetEndpoint?.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
|
||||
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</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>
|
||||
<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="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">
|
||||
<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="inputWidth" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
|
||||
[(ngModel)]="target.credential.access_secret" name="access_secret" #access_secret="ngModel"></textarea>
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
</section>
|
||||
</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>
|
@ -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>();
|
||||
|
||||
|
@ -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">
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
<clr-control-error class="position-ab" *ngIf="isLabelNameExist">
|
||||
{{'LABEL.NAME_ALREADY_EXISTS' | translate }}
|
||||
</span>
|
||||
</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>
|
||||
|
@ -62,4 +62,11 @@ form {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.clr-form-control {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
.position-ab {
|
||||
position: absolute;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -2,65 +2,74 @@
|
||||
<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>
|
||||
</label>
|
||||
<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>
|
||||
<!--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>
|
||||
<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 -->
|
||||
<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-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>
|
||||
</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>
|
||||
</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.PULL_BASED' | translate}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</clr-radio-wrapper>
|
||||
</clr-radio-container>
|
||||
<!--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">
|
||||
<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="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>
|
||||
<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>
|
||||
<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">
|
||||
<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">
|
||||
@ -68,17 +77,17 @@
|
||||
</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>
|
||||
<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" *ngIf="supportedFilters[i]?.style==='radio' && supportedFilters[i]?.values.length > 1">
|
||||
<select formControlName="value" #selectedValue id="{{'select_'+ supportedFilters[i]?.type}}" name="{{supportedFilters[i]?.type}}">
|
||||
<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" formArrayName="value">
|
||||
<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">
|
||||
@ -86,13 +95,16 @@
|
||||
</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" >
|
||||
<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>
|
||||
<div class='labelDiv'>
|
||||
<hbr-label-piece [label]="value" [labelWidth]="130"></hbr-label-piece>
|
||||
</div>
|
||||
</button>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
@ -115,56 +127,60 @@
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
</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="form-group form-group-override">
|
||||
<label class="form-group-label-override required">{{'REPLICATION.TRIGGER_MODE' | translate}}</label>
|
||||
<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">
|
||||
<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>
|
||||
@ -178,35 +194,30 @@
|
||||
</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>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</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">
|
||||
<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>
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-center">
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -14,19 +14,8 @@ h4 {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.colorRed {
|
||||
.alert-label {
|
||||
color:red;
|
||||
}
|
||||
|
||||
.colorRed a {
|
||||
text-decoration: underline;
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
.alertLabel {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
line-height: 1em;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -294,3 +280,12 @@ clr-modal {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.select-width {
|
||||
min-width:11rem;
|
||||
}
|
@ -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,
|
||||
|
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="setting-wrapper flex-layout" *ngIf="isEditMode">
|
||||
<span class="font-style">{{ labelEdit | translate }}</span>
|
||||
<div class="select select-schedule">
|
||||
<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>
|
||||
@ -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 }}
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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])
|
||||
);
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user