Address review comment

Address review comments for commit
b21f9dc6f1

and resolve conflict

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2019-09-20 13:38:57 +08:00
commit f1367064fb
259 changed files with 10777 additions and 7796 deletions

View File

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

78
SECURITY.md Normal file
View File

@ -0,0 +1,78 @@
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| Harbor v1.7.x | :white_check_mark: |
| Harbor v1.8.x | :white_check_mark: |
| Harbor v1.9.x | :white_check_mark: |
## Reporting a Vulnerability
Security is of the highest importance and all security vulnerabilities should be reported to Harbor privately, to minimize attacks against current users of Harbor before they are fixed. Vulnerabilities will be investigated and patched on the next patch (minor) release as soon as possible. This information could be kept entirely internal to the project.
To report a CVE, please email the private address cncf-harbor-security@lists.cncf.io with the details of the vulnerability. The email will be fielded by the Harbor Security Team, which is made up of Harbor maintainers who have committer and release permissions. Emails will be addressed within 2 business days (according to Beijing time), including a detailed plan to rectify the issue and workarounds to perform in the meantime. Do not report bugs through this channel. Use GitHub issues instead.
#Mailing lists
cncf-harbor-security@lists.cncf.io: for any security concerns. Received by Product Security Team members, and used by this team to discuss security issues and fixes.
cncf-harbor-distributors-announce@lists.cncf.io: for early private information on security patch releases. See below for information about how Harbor distributors can apply to join this list.
#When to report a vulnerability
* When you think Harbor has a potential security vulnerability
* When you suspect a potential vulnerability but you are unsure that it impacts Harbor
* When you know of or suspect a potential vulnerability on another project that is used by Harbor. For example, Docker, PGSql, Redis, Notary etc.
#Vulnerability Report Process
IMPORTANT: Do not file public issues on GitHub for security vulnerabilities.
To report vulnerabilities, please send an email to cncf-harbor-security@lists.cncf.io. Provide a descriptive subject line and in the body of the email include the following information:
* Basic identity information, such as your name and your affiliation or company.
* Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and compressed packet captures are all helpful to us).
* Description of the effects of the vulnerability on Harbor and the related hardware and software configurations, so that the Harbor Security Team can reproduce it.
* How the vulnerability affects Harbor usage and an estimation of the attack surface, if there is one.
* List other projects or dependencies that were used in conjunction with Harbor to produce the vulnerability..
#Patch, Release, and Disclosure
The Harbor Security Team will respond to vulnerability reports as follows:
1. The Security Team will investigate the vulnerability and determine its effects and criticality.
2. If the issue is not deemed to be a vulnerability, the Security Team will follow up with a detailed reason for rejection.
3. If a vulnerability is acknowledged and the timeline for a fix is determined, the Security Team will work on a plan to communicate with the appropriate community (to be completed within 1-7 days of the report of the vulnerability), including mitigating steps that affected users can take to protect themselves until the fix is rolled out.
4. The Security Team will also create a CVSS (https://www.first.org/cvss/specification-document) using the CVSS Calculator (https://www.first.org/cvss/calculator/3.0) . The Security Team makes the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect.
5. The Security Team works on fixing the vulnerability and performs internal testing before preparing to roll out the fix.
6. The Security Team will first email the fix to cncf-harbor-distributors-announce@lists.cncf.io, so that they can further test the fix and gather feedback. See the section Disclosure to Private Distributors List for details about how to join this mailing list.
7. Once the fix is confirmed, the Security Team will patch the vulnerability in the upcoming minor release and the next major release, and backport it into all earlier supported releases. In special cases, the fixes are cherry-picked into older unsupported releases for customers who cannot upgrade to a patched release.
8. The Security Team publishes an advisory to the Harbor community via Github, Slack, blog, and assists in rolling out the patched release to affected users.
#Disclosure to Private Distributors List
This list is intended to be used primarily to provide actionable information to multiple distributor projects at once. This list is not intended to inform individuals about security issues.
#Membership Criteria
To be eligible to join the cncf-harbor-distributors-announce@lists.cncf.io mailing list, your distribution should:
1. Be an active distributor of the Harbor component.
2. Have a user base that is not limited to your own organization.
3. Have a publicly verifiable track record up to the present day of fixing security issues.
4. Not be a downstream or rebuild of another distributor.
5. Be a participant and active contributor in the Harbor community.
6. Accept the Embargo Policy that is outlined below.
7. Have someone who is already on the list vouch for the person requesting membership on behalf of your distribution.
#Embargo Policy
The information that members receive on cncf-harbor-distributors-announce@lists.cncf.io must not be made public, shared, or even hinted at anywhere beyond those who need to know within your specific team, unless you receive explicit approval to do so from the list. This remains true until the public disclosure date/time agreed upon by the list. Members of the list and others cannot use the information for any reason other than to get the issue fixed for your respective distribution's users.
Before you share any information from the list with members of your team who are required to fix the issue, these team members must agree to the same terms, and only be provided with information on a need-to-know basis.
In the unfortunate event that you share information beyond what is permitted by this policy, you must urgently inform the cncf-harbor-security@lists.cncf.io mailing list of exactly what information was leaked and to whom.
If you continue to leak information and break the policy outlined here, you will be removed from the list.
#Requesting to Join
Send new membership requests to cncf-harbor-security@lists.cncf.io.
In the body of your request please specify how you qualify for membership and fulfill each criterion listed in the Membership Criteria section above.

View File

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

View File

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

View File

@ -6,6 +6,7 @@ COPY ./LICENSE /portal_src
WORKDIR /build_dir WORKDIR /build_dir
RUN cp -r /portal_src/* /build_dir \ RUN cp -r /portal_src/* /build_dir \
&& ls -la \ && ls -la \
&& apt-get update \ && apt-get update \
@ -14,7 +15,7 @@ RUN cp -r /portal_src/* /build_dir \
&& npm install \ && npm install \
&& npm run build_lib \ && npm run build_lib \
&& npm run link_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 FROM photon:2.0

View File

@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/goharbor/harbor/src/common/models"
"net/http" "net/http"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
@ -37,6 +38,7 @@ import (
const ( const (
yamlFileContentType = "application/x-yaml" yamlFileContentType = "application/x-yaml"
userSessionKey = "user"
) )
// the managers/controllers used globally // the managers/controllers used globally
@ -168,6 +170,12 @@ func (b *BaseController) WriteYamlData(object interface{}) {
_, _ = w.Write(yData) _, _ = w.Write(yData)
} }
// PopulateUserSession generates a new session ID and fill the user model in parm to the session
func (b *BaseController) PopulateUserSession(u models.User) {
b.SessionRegenerateID()
b.SetSession(userSessionKey, u)
}
// Init related objects/configurations for the API controllers // Init related objects/configurations for the API controllers
func Init() error { func Init() error {
registerHealthCheckers() registerHealthCheckers()

View File

@ -206,6 +206,13 @@ func init() {
beego.Router("/api/internal/switchquota", &InternalAPI{}, "put:SwitchQuota") beego.Router("/api/internal/switchquota", &InternalAPI{}, "put:SwitchQuota")
beego.Router("/api/internal/syncquota", &InternalAPI{}, "post:SyncQuota") 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 // syncRegistry
if err := SyncRegistry(config.GlobalProjectMgr); err != nil { if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
log.Fatalf("failed to sync repositories from registry: %v", err) log.Fatalf("failed to sync repositories from registry: %v", err)

View File

@ -0,0 +1,348 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/pkg/errors"
)
// ScannerAPI provides the API for managing the plugin scanners
type ScannerAPI struct {
// The base controller to provide common utilities
BaseController
// Controller for the plug scanners
c api.Controller
}
// Prepare sth. for the subsequent actions
func (sa *ScannerAPI) Prepare() {
// Call super prepare method
sa.BaseController.Prepare()
// Check access permissions
if !sa.SecurityCtx.IsAuthenticated() {
sa.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
if !sa.SecurityCtx.IsSysAdmin() {
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
return
}
// Use the default controller
sa.c = api.DefaultController
}
// Get the specified scanner
func (sa *ScannerAPI) Get() {
if r := sa.get(); r != nil {
// Response to the client
sa.Data["json"] = r
sa.ServeJSON()
}
}
// List all the scanners
func (sa *ScannerAPI) List() {
p, pz, err := sa.GetPaginationParams()
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: list all"))
return
}
query := &q.Query{
PageSize: pz,
PageNumber: p,
}
// Get query key words
kws := make(map[string]string)
properties := []string{"name", "description", "url"}
for _, k := range properties {
kw := sa.GetString(k)
if len(kw) > 0 {
kws[k] = kw
}
}
if len(kws) > 0 {
query.Keywords = kws
}
all, err := sa.c.ListRegistrations(query)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: list all"))
return
}
// Response to the client
sa.Data["json"] = all
sa.ServeJSON()
}
// Create a new scanner
func (sa *ScannerAPI) Create() {
r := &scanner.Registration{}
if err := sa.DecodeJSONReq(r); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
return
}
if err := r.Validate(false); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
return
}
// Explicitly check if conflict
if !sa.checkDuplicated("name", r.Name) ||
!sa.checkDuplicated("url", r.URL) {
return
}
// All newly created should be non default one except the 1st one
r.IsDefault = false
uuid, err := sa.c.CreateRegistration(r)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: create"))
return
}
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, uuid)
sa.Ctx.ResponseWriter.Header().Add("Location", location)
resp := make(map[string]string, 1)
resp["uuid"] = uuid
// Response to the client
sa.Ctx.ResponseWriter.WriteHeader(http.StatusCreated)
sa.Data["json"] = resp
sa.ServeJSON()
}
// Update a scanner
func (sa *ScannerAPI) Update() {
r := sa.get()
if r == nil {
// meet error
return
}
// full dose updated
rr := &scanner.Registration{}
if err := sa.DecodeJSONReq(rr); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
return
}
if err := r.Validate(true); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
return
}
// Name changed?
if r.Name != rr.Name {
if !sa.checkDuplicated("name", rr.Name) {
return
}
}
// URL changed?
if r.URL != rr.URL {
if !sa.checkDuplicated("url", rr.URL) {
return
}
}
getChanges(r, rr)
if err := sa.c.UpdateRegistration(r); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: update"))
return
}
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, r.UUID)
sa.Ctx.ResponseWriter.Header().Add("Location", location)
// Response to the client
sa.Data["json"] = r
sa.ServeJSON()
}
// Delete the scanner
func (sa *ScannerAPI) Delete() {
uid := sa.GetStringFromPath(":uid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return
}
deleted, err := sa.c.DeleteRegistration(uid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: delete"))
return
}
if deleted == nil {
// Not found
sa.SendNotFoundError(errors.Errorf("scanner registration: %s", uid))
return
}
sa.Data["json"] = deleted
sa.ServeJSON()
}
// SetAsDefault sets the given registration as default one
func (sa *ScannerAPI) SetAsDefault() {
uid := sa.GetStringFromPath(":uid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return
}
m := make(map[string]interface{})
if err := sa.DecodeJSONReq(&m); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set as default"))
return
}
if v, ok := m["is_default"]; ok {
if isDefault, y := v.(bool); y && isDefault {
if err := sa.c.SetDefaultRegistration(uid); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set as default"))
}
return
}
}
// Not supported
sa.SendForbiddenError(errors.Errorf("not supported: %#v", m))
}
// GetProjectScanner gets the project level scanner
func (sa *ScannerAPI) GetProjectScanner() {
pid, err := sa.GetInt64FromPath(":pid")
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: get project scanners"))
return
}
r, err := sa.c.GetRegistrationByProject(pid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get project scanners"))
return
}
if r != nil {
sa.Data["json"] = r
} else {
sa.Data["json"] = make(map[string]interface{})
}
sa.ServeJSON()
}
// SetProjectScanner sets the project level scanner
func (sa *ScannerAPI) SetProjectScanner() {
pid, err := sa.GetInt64FromPath(":pid")
if err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
body := make(map[string]string)
if err := sa.DecodeJSONReq(&body); err != nil {
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
uuid, ok := body["uuid"]
if !ok || len(uuid) == 0 {
sa.SendBadRequestError(errors.New("missing scanner uuid when setting project scanner"))
return
}
if err := sa.c.SetRegistrationByProject(pid, uuid); err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set project scanners"))
return
}
}
// get the specified scanner
func (sa *ScannerAPI) get() *scanner.Registration {
uid := sa.GetStringFromPath(":uid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return nil
}
r, err := sa.c.GetRegistration(uid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get"))
return nil
}
if r == nil {
// NOT found
sa.SendNotFoundError(errors.Errorf("scanner: %s", uid))
return nil
}
return r
}
func (sa *ScannerAPI) checkDuplicated(property, value string) bool {
// Explicitly check if conflict
kw := make(map[string]string)
kw[property] = value
query := &q.Query{
Keywords: kw,
}
l, err := sa.c.ListRegistrations(query)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scanner API: check existence"))
return false
}
if len(l) > 0 {
sa.SendConflictError(errors.Errorf("duplicated entries: %s:%s", property, value))
return false
}
return true
}
func getChanges(e *scanner.Registration, eChange *scanner.Registration) {
e.Name = eChange.Name
e.Description = eChange.Description
e.URL = eChange.URL
e.Auth = eChange.Auth
e.AccessCredential = eChange.AccessCredential
e.Disabled = eChange.Disabled
e.SkipCertVerify = eChange.SkipCertVerify
}

View File

@ -0,0 +1,444 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"testing"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
const (
rootRoute = "/api/scanners"
)
// ScannerAPITestSuite is test suite for testing the scanner API
type ScannerAPITestSuite struct {
suite.Suite
originC api.Controller
mockC *MockScannerAPIController
}
// TestScannerAPI is the entry of ScannerAPITestSuite
func TestScannerAPI(t *testing.T) {
suite.Run(t, new(ScannerAPITestSuite))
}
// SetupSuite prepares testing env
func (suite *ScannerAPITestSuite) SetupTest() {
suite.originC = api.DefaultController
m := &MockScannerAPIController{}
api.DefaultController = m
suite.mockC = m
}
// TearDownTest clears test case env
func (suite *ScannerAPITestSuite) TearDownTest() {
// Restore
api.DefaultController = suite.originC
}
// TestScannerAPICreate tests the post request to create new one
func (suite *ScannerAPITestSuite) TestScannerAPIBase() {
// Including general cases
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 400
{
request: &testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: sysAdmin,
bodyJSON: &scanner.Registration{
URL: "http://a.b.c",
},
},
code: http.StatusBadRequest,
},
}
runCodeCheckingCases(suite.T(), cases...)
}
// TestScannerAPIGet tests api get
func (suite *ScannerAPITestSuite) TestScannerAPIGet() {
res := &scanner.Registration{
ID: 1000,
UUID: "uuid",
Name: "TestScannerAPIGet",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("GetRegistration", "uuid").Return(res, nil)
// Get
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodGet,
credential: sysAdmin,
}, rr)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), rr)
assert.Equal(suite.T(), res.Name, rr.Name)
assert.Equal(suite.T(), res.UUID, rr.UUID)
}
// TestScannerAPICreate tests create.
func (suite *ScannerAPITestSuite) TestScannerAPICreate() {
r := &scanner.Registration{
Name: "TestScannerAPICreate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockQuery(r)
suite.mockC.On("CreateRegistration", r).Return("uuid", nil)
// Create
res := make(map[string]string, 1)
err := handleAndParse(
&testingRequest{
url: rootRoute,
method: http.MethodPost,
credential: sysAdmin,
bodyJSON: r,
}, &res)
require.NoError(suite.T(), err)
require.Condition(suite.T(), func() (success bool) {
success = res["uuid"] == "uuid"
return
})
}
// TestScannerAPIList tests list
func (suite *ScannerAPITestSuite) TestScannerAPIList() {
query := &q.Query{
PageNumber: 1,
PageSize: 500,
}
ll := []*scanner.Registration{
{
ID: 1001,
UUID: "uuid",
Name: "TestScannerAPIList",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}}
suite.mockC.On("ListRegistrations", query).Return(ll, nil)
// List
l := make([]*scanner.Registration, 0)
err := handleAndParse(&testingRequest{
url: rootRoute,
method: http.MethodGet,
credential: sysAdmin,
}, &l)
require.NoError(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) {
success = len(l) > 0 && l[0].Name == ll[0].Name
return
})
}
// TestScannerAPIUpdate tests the update API
func (suite *ScannerAPITestSuite) TestScannerAPIUpdate() {
before := &scanner.Registration{
ID: 1002,
UUID: "uuid",
Name: "TestScannerAPIUpdate_before",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
updated := &scanner.Registration{
ID: 1002,
UUID: "uuid",
Name: "TestScannerAPIUpdate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockQuery(updated)
suite.mockC.On("UpdateRegistration", updated).Return(nil)
suite.mockC.On("GetRegistration", "uuid").Return(before, nil)
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodPut,
credential: sysAdmin,
bodyJSON: updated,
}, rr)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), rr)
assert.Equal(suite.T(), updated.Name, rr.Name)
assert.Equal(suite.T(), updated.UUID, rr.UUID)
}
//
func (suite *ScannerAPITestSuite) TestScannerAPIDelete() {
r := &scanner.Registration{
ID: 1003,
UUID: "uuid",
Name: "TestScannerAPIDelete",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("DeleteRegistration", "uuid").Return(r, nil)
deleted := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodDelete,
credential: sysAdmin,
}, deleted)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), r.UUID, deleted.UUID)
assert.Equal(suite.T(), r.Name, deleted.Name)
}
// TestScannerAPISetDefault tests the set default
func (suite *ScannerAPITestSuite) TestScannerAPISetDefault() {
suite.mockC.On("SetDefaultRegistration", "uuid").Return(nil)
body := make(map[string]interface{}, 1)
body["is_default"] = true
runCodeCheckingCases(suite.T(), &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
method: http.MethodPatch,
credential: sysAdmin,
bodyJSON: body,
},
code: http.StatusOK,
})
}
// TestScannerAPIProjectScanner tests the API of getting/setting project level scanner
func (suite *ScannerAPITestSuite) TestScannerAPIProjectScanner() {
suite.mockC.On("SetRegistrationByProject", int64(1), "uuid").Return(nil)
// Set
body := make(map[string]interface{}, 1)
body["uuid"] = "uuid"
runCodeCheckingCases(suite.T(), &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
method: http.MethodPut,
credential: sysAdmin,
bodyJSON: body,
},
code: http.StatusOK,
})
r := &scanner.Registration{
ID: 1004,
UUID: "uuid",
Name: "TestScannerAPIProjectScanner",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("GetRegistrationByProject", int64(1)).Return(r, nil)
// Get
rr := &scanner.Registration{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
method: http.MethodGet,
credential: sysAdmin,
}, rr)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), r.Name, rr.Name)
assert.Equal(suite.T(), r.UUID, rr.UUID)
}
func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
kw := make(map[string]string, 1)
kw["name"] = r.Name
query := &q.Query{
Keywords: kw,
}
emptyL := make([]*scanner.Registration, 0)
suite.mockC.On("ListRegistrations", query).Return(emptyL, nil)
kw2 := make(map[string]string, 1)
kw2["url"] = r.URL
query2 := &q.Query{
Keywords: kw2,
}
suite.mockC.On("ListRegistrations", query2).Return(emptyL, nil)
}
// MockScannerAPIController is mock of scanner API controller
type MockScannerAPIController struct {
mock.Mock
}
// ListRegistrations ...
func (m *MockScannerAPIController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) {
args := m.Called(query)
return args.Get(0).([]*scanner.Registration), args.Error(1)
}
// CreateRegistration ...
func (m *MockScannerAPIController) CreateRegistration(registration *scanner.Registration) (string, error) {
args := m.Called(registration)
return args.String(0), args.Error(1)
}
// GetRegistration ...
func (m *MockScannerAPIController) GetRegistration(registrationUUID string) (*scanner.Registration, error) {
args := m.Called(registrationUUID)
s := args.Get(0)
if s == nil {
return nil, args.Error(1)
}
return s.(*scanner.Registration), args.Error(1)
}
// RegistrationExists ...
func (m *MockScannerAPIController) RegistrationExists(registrationUUID string) bool {
args := m.Called(registrationUUID)
return args.Bool(0)
}
// UpdateRegistration ...
func (m *MockScannerAPIController) UpdateRegistration(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// DeleteRegistration ...
func (m *MockScannerAPIController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) {
args := m.Called(registrationUUID)
s := args.Get(0)
if s == nil {
return nil, args.Error(1)
}
return s.(*scanner.Registration), args.Error(1)
}
// SetDefaultRegistration ...
func (m *MockScannerAPIController) SetDefaultRegistration(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// SetRegistrationByProject ...
func (m *MockScannerAPIController) SetRegistrationByProject(projectID int64, scannerID string) error {
args := m.Called(projectID, scannerID)
return args.Error(0)
}
// GetRegistrationByProject ...
func (m *MockScannerAPIController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) {
args := m.Called(projectID)
s := args.Get(0)
if s == nil {
return nil, args.Error(1)
}
return s.(*scanner.Registration), args.Error(1)
}
// Ping ...
func (m *MockScannerAPIController) Ping(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// Scan ...
func (m *MockScannerAPIController) Scan(artifact *scan.Artifact) error {
args := m.Called(artifact)
return args.Error(0)
}
// GetReport ...
func (m *MockScannerAPIController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
args := m.Called(artifact)
r := args.Get(0)
if r == nil {
return nil, args.Error(1)
}
return r.([]*dscan.Report), args.Error(1)
}
// GetScanLog ...
func (m *MockScannerAPIController) GetScanLog(digest string) ([]byte, error) {
args := m.Called(digest)
l := args.Get(0)
if l == nil {
return nil, args.Error(1)
}
return l.([]byte), args.Error(1)
}

View File

@ -17,6 +17,7 @@ package controllers
import ( import (
"bytes" "bytes"
"context" "context"
"github.com/goharbor/harbor/src/core/api"
"html/template" "html/template"
"net" "net"
"net/http" "net/http"
@ -38,11 +39,9 @@ import (
"github.com/goharbor/harbor/src/core/filter" "github.com/goharbor/harbor/src/core/filter"
) )
const userKey = "user"
// CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ... // CommonController handles request from UI that doesn't expect a page, such as /SwitchLanguage /logout ...
type CommonController struct { type CommonController struct {
beego.Controller api.BaseController
i18n.Locale i18n.Locale
} }
@ -51,6 +50,9 @@ func (cc *CommonController) Render() error {
return nil return nil
} }
// Prepare overwrites the Prepare func in api.BaseController to ignore unnecessary steps
func (cc *CommonController) Prepare() {}
type messageDetail struct { type messageDetail struct {
Hint string Hint string
URL string URL string
@ -111,7 +113,7 @@ func (cc *CommonController) Login() {
if user == nil { if user == nil {
cc.CustomAbort(http.StatusUnauthorized, "") cc.CustomAbort(http.StatusUnauthorized, "")
} }
cc.SetSession(userKey, *user) cc.PopulateUserSession(*user)
} }
// LogOut Habor UI // LogOut Habor UI

View File

@ -155,7 +155,7 @@ func (oc *OIDCController) Callback() {
oc.SendInternalServerError(err) oc.SendInternalServerError(err)
return return
} }
oc.SetSession(userKey, *u) oc.PopulateUserSession(*u)
oc.Controller.Redirect("/", http.StatusFound) oc.Controller.Redirect("/", http.StatusFound)
} }
} }
@ -182,7 +182,6 @@ func (oc *OIDCController) Onboard() {
oc.SendBadRequestError(errors.New("Failed to get OIDC user info from session")) oc.SendBadRequestError(errors.New("Failed to get OIDC user info from session"))
return return
} }
defer oc.DelSession(userInfoKey)
log.Debugf("User info string: %s\n", userInfoStr) log.Debugf("User info string: %s\n", userInfoStr)
tb, ok := oc.GetSession(tokenKey).([]byte) tb, ok := oc.GetSession(tokenKey).([]byte)
if !ok { if !ok {
@ -223,11 +222,13 @@ func (oc *OIDCController) Onboard() {
return return
} }
oc.SendInternalServerError(err) oc.SendInternalServerError(err)
oc.DelSession(userInfoKey)
return return
} }
user.OIDCUserMeta = nil user.OIDCUserMeta = nil
oc.SetSession(userKey, user) oc.DelSession(userInfoKey)
oc.PopulateUserSession(user)
} }
func secretAndToken(tokenBytes []byte) (string, string, error) { func secretAndToken(tokenBytes []byte) (string, string, error) {

View File

@ -192,6 +192,13 @@ func initRouters() {
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel") 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 // Error pages
beego.ErrorController(&controllers.ErrorController{}) beego.ErrorController(&controllers.ErrorController{})

View File

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

View File

@ -140,6 +140,8 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeq
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/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/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/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/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 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ package doublestar
import ( import (
"fmt" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -28,7 +28,7 @@ import (
type RegExpSelectorTestSuite struct { type RegExpSelectorTestSuite struct {
suite.Suite suite.Suite
artifacts []*res.Candidate artifacts []*art.Candidate
} }
// TestRegExpSelector is entrance for RegExpSelectorTestSuite // TestRegExpSelector is entrance for RegExpSelectorTestSuite
@ -38,13 +38,13 @@ func TestRegExpSelector(t *testing.T) {
// SetupSuite to do preparation work // SetupSuite to do preparation work
func (suite *RegExpSelectorTestSuite) SetupSuite() { func (suite *RegExpSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{ suite.artifacts = []*art.Candidate{
{ {
NamespaceID: 1, NamespaceID: 1,
Namespace: "library", Namespace: "library",
Repository: "harbor", Repository: "harbor",
Tag: "latest", Tag: "latest",
Kind: res.Image, Kind: art.Image,
PushedTime: time.Now().Unix() - 3600, PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(), PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200, CreationTime: time.Now().Unix() - 7200,
@ -55,7 +55,7 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
Namespace: "retention", Namespace: "retention",
Repository: "redis", Repository: "redis",
Tag: "4.0", Tag: "4.0",
Kind: res.Image, Kind: art.Image,
PushedTime: time.Now().Unix() - 3600, PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(), PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200, CreationTime: time.Now().Unix() - 7200,
@ -66,7 +66,7 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
Namespace: "retention", Namespace: "retention",
Repository: "redis", Repository: "redis",
Tag: "4.1", Tag: "4.1",
Kind: res.Image, Kind: art.Image,
PushedTime: time.Now().Unix() - 3600, PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(), PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200, 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) // 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) hash := make(map[string]bool)
for _, art := range candidates { for _, art := range candidates {

View File

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

View File

@ -17,7 +17,7 @@ package label
import ( import (
"strings" "strings"
"github.com/goharbor/harbor/src/pkg/retention/res" "github.com/goharbor/harbor/src/pkg/art"
) )
const ( const (
@ -39,7 +39,7 @@ type selector struct {
} }
// Select candidates by the labels // 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 { for _, art := range artifacts {
if isMatched(s.labels, art.Labels, s.decoration) { if isMatched(s.labels, art.Labels, s.decoration) {
selected = append(selected, art) 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 // 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) labels := make([]string, 0)
if len(pattern) > 0 { if len(pattern) > 0 {
labels = append(labels, strings.Split(pattern, ",")...) labels = append(labels, strings.Split(pattern, ",")...)

View File

@ -16,7 +16,7 @@ package label
import ( import (
"fmt" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -28,7 +28,7 @@ import (
type LabelSelectorTestSuite struct { type LabelSelectorTestSuite struct {
suite.Suite suite.Suite
artifacts []*res.Candidate artifacts []*art.Candidate
} }
// TestLabelSelector is entrance for LabelSelectorTestSuite // TestLabelSelector is entrance for LabelSelectorTestSuite
@ -38,13 +38,13 @@ func TestLabelSelector(t *testing.T) {
// SetupSuite to do preparation work // SetupSuite to do preparation work
func (suite *LabelSelectorTestSuite) SetupSuite() { func (suite *LabelSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{ suite.artifacts = []*art.Candidate{
{ {
NamespaceID: 1, NamespaceID: 1,
Namespace: "library", Namespace: "library",
Repository: "harbor", Repository: "harbor",
Tag: "1.9", Tag: "1.9",
Kind: res.Image, Kind: art.Image,
PushedTime: time.Now().Unix() - 3600, PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(), PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200, CreationTime: time.Now().Unix() - 7200,
@ -55,7 +55,7 @@ func (suite *LabelSelectorTestSuite) SetupSuite() {
Namespace: "library", Namespace: "library",
Repository: "harbor", Repository: "harbor",
Tag: "dev", Tag: "dev",
Kind: res.Image, Kind: art.Image,
PushedTime: time.Now().Unix() - 3600, PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(), PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200, 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) // 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) hash := make(map[string]bool)
for _, art := range candidates { for _, art := range candidates {

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

@ -0,0 +1,25 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package q
// Query parameters
type Query struct {
// Page number
PageNumber int64
// Page size
PageSize int64
// List of key words
Keywords map[string]string
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,12 +21,12 @@ import (
"github.com/goharbor/harbor/src/chartserver" "github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/job" "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/models" "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/project"
"github.com/goharbor/harbor/src/pkg/repository" "github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy" "github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/q" "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" hjob "github.com/goharbor/harbor/src/testing/job"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,13 +21,13 @@ import (
index3 "github.com/goharbor/harbor/src/pkg/retention/policy/alg/index" 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/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/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp" "github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -46,7 +46,7 @@ type Builder interface {
} }
// NewBuilder news a basic builder // NewBuilder news a basic builder
func NewBuilder(all []*res.Candidate) Builder { func NewBuilder(all []*art.Candidate) Builder {
return &basicBuilder{ return &basicBuilder{
allCandidates: all, allCandidates: all,
} }
@ -54,7 +54,7 @@ func NewBuilder(all []*res.Candidate) Builder {
// basicBuilder is default implementation of Builder interface // basicBuilder is default implementation of Builder interface
type basicBuilder struct { type basicBuilder struct {
allCandidates []*res.Candidate allCandidates []*art.Candidate
} }
// Build policy processor from the raw policy // 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") 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 { for _, s := range r.TagSelectors {
sel, err := index2.Get(s.Kind, s.Decoration, s.Pattern) sel, err := index2.Get(s.Kind, s.Decoration, s.Pattern)
if err != nil { if err != nil {

View File

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

View File

@ -15,9 +15,9 @@
package always package always
import ( 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/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
) )
const ( const (
@ -28,7 +28,7 @@ const (
type evaluator struct{} type evaluator struct{}
// Process for the "always" Evaluator simply returns the input with no error // 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 return artifacts, nil
} }

View File

@ -17,8 +17,8 @@ package always
import ( import (
"testing" "testing"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "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/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -36,7 +36,7 @@ func (e *EvaluatorTestSuite) TestNew() {
func (e *EvaluatorTestSuite) TestProcess() { func (e *EvaluatorTestSuite) TestProcess() {
sut := New(rule.Parameters{}) 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) result, err := sut.Process(input)

View File

@ -20,9 +20,9 @@ import (
"time" "time"
"github.com/goharbor/harbor/src/common/utils/log" "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/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
) )
const ( const (
@ -41,7 +41,7 @@ type evaluator struct {
n int 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() minPullTime := time.Now().UTC().Add(time.Duration(-1*24*e.n) * time.Hour).Unix()
for _, a := range artifacts { for _, a := range artifacts {
if a.PulledTime >= minPullTime { if a.PulledTime >= minPullTime {

View File

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

View File

@ -20,9 +20,9 @@ import (
"time" "time"
"github.com/goharbor/harbor/src/common/utils/log" "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/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
) )
const ( const (
@ -41,7 +41,7 @@ type evaluator struct {
n int 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() minPushTime := time.Now().UTC().Add(time.Duration(-1*24*e.n) * time.Hour).Unix()
for _, a := range artifacts { for _, a := range artifacts {
if a.PushedTime >= minPushTime { if a.PushedTime >= minPushTime {

View File

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

View File

@ -14,19 +14,19 @@
package rule 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 // Evaluator defines method of executing rule
type Evaluator interface { type Evaluator interface {
// Filter the inputs and return the filtered outputs // Filter the inputs and return the filtered outputs
// //
// Arguments: // Arguments:
// artifacts []*res.Candidate : candidates for processing // artifacts []*art.Candidate : candidates for processing
// //
// Returns: // Returns:
// []*res.Candidate : matched candidates for next stage // []*art.Candidate : matched candidates for next stage
// error : common error object if any errors occurred // 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 // Specify what action is performed to the candidates processed by this evaluator
Action() string Action() string

View File

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

View File

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

View File

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

View File

@ -19,9 +19,9 @@ import (
"sort" "sort"
"github.com/goharbor/harbor/src/common/utils/log" "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/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
) )
const ( const (
@ -40,7 +40,7 @@ type evaluator struct {
} }
// Process the candidates based on the rule definition // 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" // Sort artifacts by their "active time"
// //
// Active time is defined as the selection of c.PulledTime or c.PushedTime, // 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 { if c.PulledTime > c.PushedTime {
return c.PulledTime return c.PulledTime
} }

View File

@ -22,18 +22,18 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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" "github.com/stretchr/testify/suite"
) )
type EvaluatorTestSuite struct { type EvaluatorTestSuite struct {
suite.Suite suite.Suite
artifacts []*res.Candidate artifacts []*art.Candidate
} }
func (e *EvaluatorTestSuite) SetupSuite() { func (e *EvaluatorTestSuite) SetupSuite() {
e.artifacts = []*res.Candidate{ e.artifacts = []*art.Candidate{
{PulledTime: 1, PushedTime: 2}, {PulledTime: 1, PushedTime: 2},
{PulledTime: 3, PushedTime: 4}, {PulledTime: 3, PushedTime: 4},
{PulledTime: 6, PushedTime: 5}, {PulledTime: 6, PushedTime: 5},

View File

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

View File

@ -20,8 +20,8 @@ import (
"math/rand" "math/rand"
"testing" "testing"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "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/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -52,7 +52,7 @@ func (e *EvaluatorTestSuite) TestNew() {
} }
func (e *EvaluatorTestSuite) TestProcess() { 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) { rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i] data[i], data[j] = data[j], data[i]
}) })

View File

@ -21,9 +21,9 @@ import (
"sort" "sort"
"github.com/goharbor/harbor/src/common/utils/log" "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/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
) )
const ( const (
@ -42,7 +42,7 @@ type evaluator struct {
} }
// Process the candidates based on the rule definition // 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 // 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 { sort.Slice(artifacts, func(i, j int) bool {
return artifacts[i].PushedTime > artifacts[j].PushedTime return artifacts[i].PushedTime > artifacts[j].PushedTime

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/suite" "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/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -39,7 +39,7 @@ func (e *EvaluatorTestSuite) TestNew() {
} }
func (e *EvaluatorTestSuite) TestProcess() { 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) { rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i] data[i], data[j] = data[j], data[i]
}) })

View File

@ -15,9 +15,9 @@
package nothing package nothing
import ( 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/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
) )
const ( const (
@ -28,7 +28,7 @@ const (
type evaluator struct{} type evaluator struct{}
// Process for the "nothing" Evaluator simply returns the input with no error // 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 return processed, err
} }

View File

@ -17,8 +17,8 @@ package nothing
import ( import (
"testing" "testing"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "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/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -36,7 +36,7 @@ func (e *EvaluatorTestSuite) TestNew() {
func (e *EvaluatorTestSuite) TestProcess() { func (e *EvaluatorTestSuite) TestProcess() {
sut := New(rule.Parameters{}) 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) result, err := sut.Process(input)

View File

@ -0,0 +1,157 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"github.com/goharbor/harbor/src/pkg/q"
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
)
// Controller provides the related operations of scanner for the upper API.
// All the capabilities of the scanner are defined here.
type Controller interface {
// ListRegistrations returns a list of currently configured scanner registrations.
// Query parameters are optional
//
// Arguments:
// query *q.Query : query parameters
//
// Returns:
// []*scanner.Registration : scanner list of all the matched ones
// error : non nil error if any errors occurred
ListRegistrations(query *q.Query) ([]*scanner.Registration, error)
// CreateRegistration creates a new scanner registration with the given data.
// Returns the scanner registration identifier.
//
// Arguments:
// registration *scanner.Registration : scanner registration to create
//
// Returns:
// string : the generated UUID of the new scanner
// error : non nil error if any errors occurred
CreateRegistration(registration *scanner.Registration) (string, error)
// GetRegistration returns the details of the specified scanner registration.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner
//
// Returns:
// *scanner.Registration : the required scanner
// error : non nil error if any errors occurred
GetRegistration(registrationUUID string) (*scanner.Registration, error)
// RegistrationExists checks if the provided registration is there.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner
//
// Returns:
// true for existing or false for not existing
RegistrationExists(registrationUUID string) bool
// UpdateRegistration updates the specified scanner registration.
//
// Arguments:
// registration *scanner.Registration : scanner registration to update
//
// Returns:
// error : non nil error if any errors occurred
UpdateRegistration(registration *scanner.Registration) error
// DeleteRegistration deletes the specified scanner registration.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner which is going to be deleted
//
// Returns:
// *scanner.Registration : the deleted scanner
// error : non nil error if any errors occurred
DeleteRegistration(registrationUUID string) (*scanner.Registration, error)
// SetDefaultRegistration marks the specified scanner registration as default.
// The implementation is supposed to unset any registration previously set as default.
//
// Arguments:
// registrationUUID string : the UUID of the given scanner which is marked as default
//
// Returns:
// error : non nil error if any errors occurred
SetDefaultRegistration(registrationUUID string) error
// SetRegistrationByProject sets scanner for the given project.
//
// Arguments:
// projectID int64 : the ID of the given project
// scannerID string : the UUID of the the scanner
//
// Returns:
// error : non nil error if any errors occurred
SetRegistrationByProject(projectID int64, scannerID string) error
// GetRegistrationByProject returns the configured scanner registration of the given project or
// the system default registration if exists or `nil` if no system registrations set.
//
// Arguments:
// projectID int64 : the ID of the given project
//
// Returns:
// *scanner.Registration : the default scanner registration
// error : non nil error if any errors occurred
GetRegistrationByProject(projectID int64) (*scanner.Registration, error)
// Ping pings Scanner Adapter to test EndpointURL and Authorization settings.
// The implementation is supposed to call the GetMetadata method on scanner.Client.
// Returns `nil` if connection succeeded, a non `nil` error otherwise.
//
// Arguments:
// registration *scanner.Registration : scanner registration to ping
//
// Returns:
// error : non nil error if any errors occurred
Ping(registration *scanner.Registration) error
// Scan the given artifact
//
// Arguments:
// artifact *res.Artifact : artifact to be scanned
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *scan.Artifact) error
// GetReport gets the reports for the given artifact identified by the digest
//
// Arguments:
// artifact *res.Artifact : the scanned artifact
//
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *scan.Artifact) ([]*dscan.Report, error)
// Get the scan log for the specified artifact with the given digest
//
// Arguments:
// digest string : the digest of the artifact
//
// Returns:
// []byte : the log text stream
// error : non nil error if any errors occurred
GetScanLog(digest string) ([]byte, error)
}

View File

@ -0,0 +1,287 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ControllerTestSuite is test suite to test the basic api controller.
type ControllerTestSuite struct {
suite.Suite
c *basicController
mMgr *MockScannerManager
mMeta *MockProMetaManager
sample *scanner.Registration
}
// TestController is the entry of controller test suite
func TestController(t *testing.T) {
suite.Run(t, new(ControllerTestSuite))
}
// SetupSuite prepares env for the controller test suite
func (suite *ControllerTestSuite) SetupSuite() {
suite.mMgr = new(MockScannerManager)
suite.mMeta = new(MockProMetaManager)
suite.c = &basicController{
manager: suite.mMgr,
proMetaMgr: suite.mMeta,
}
suite.sample = &scanner.Registration{
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
}
// Clear test case
func (suite *ControllerTestSuite) TearDownTest() {
suite.sample.UUID = ""
}
// TestListRegistrations tests ListRegistrations
func (suite *ControllerTestSuite) TestListRegistrations() {
query := &q.Query{
PageSize: 10,
PageNumber: 1,
}
suite.sample.UUID = "uuid"
l := []*scanner.Registration{suite.sample}
suite.mMgr.On("List", query).Return(l, nil)
rl, err := suite.c.ListRegistrations(query)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(rl))
}
// TestCreateRegistration tests CreateRegistration
func (suite *ControllerTestSuite) TestCreateRegistration() {
suite.mMgr.On("Create", suite.sample).Return("uuid", nil)
uid, err := suite.mMgr.Create(suite.sample)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), uid, "uuid")
}
// TestGetRegistration tests GetRegistration
func (suite *ControllerTestSuite) TestGetRegistration() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
rr, err := suite.c.GetRegistration("uuid")
require.NoError(suite.T(), err)
assert.NotNil(suite.T(), rr)
assert.Equal(suite.T(), "forUT", rr.Name)
}
// TestRegistrationExists tests RegistrationExists
func (suite *ControllerTestSuite) TestRegistrationExists() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
exists := suite.c.RegistrationExists("uuid")
assert.Equal(suite.T(), true, exists)
suite.mMgr.On("Get", "uuid2").Return(nil, nil)
exists = suite.c.RegistrationExists("uuid2")
assert.Equal(suite.T(), false, exists)
}
// TestUpdateRegistration tests UpdateRegistration
func (suite *ControllerTestSuite) TestUpdateRegistration() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Update", suite.sample).Return(nil)
err := suite.c.UpdateRegistration(suite.sample)
require.NoError(suite.T(), err)
}
// TestDeleteRegistration tests DeleteRegistration
func (suite *ControllerTestSuite) TestDeleteRegistration() {
suite.sample.UUID = "uuid"
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
suite.mMgr.On("Delete", "uuid").Return(nil)
r, err := suite.c.DeleteRegistration("uuid")
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "forUT", r.Name)
}
// TestSetDefaultRegistration tests SetDefaultRegistration
func (suite *ControllerTestSuite) TestSetDefaultRegistration() {
suite.mMgr.On("SetAsDefault", "uuid").Return(nil)
err := suite.c.SetDefaultRegistration("uuid")
require.NoError(suite.T(), err)
}
// TestSetRegistrationByProject tests SetRegistrationByProject
func (suite *ControllerTestSuite) TestSetRegistrationByProject() {
m := make(map[string]string, 1)
mm := make(map[string]string, 1)
mmm := make(map[string]string, 1)
mm[proScannerMetaKey] = "uuid"
mmm[proScannerMetaKey] = "uuid2"
var pid, pid2 int64 = 1, 2
// not set before
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil)
suite.mMeta.On("Add", pid, mm).Return(nil)
err := suite.c.SetRegistrationByProject(pid, "uuid")
require.NoError(suite.T(), err)
// Set before
suite.mMeta.On("Get", pid2, []string{proScannerMetaKey}).Return(mm, nil)
suite.mMeta.On("Update", pid2, mmm).Return(nil)
err = suite.c.SetRegistrationByProject(pid2, "uuid2")
require.NoError(suite.T(), err)
}
// TestGetRegistrationByProject tests GetRegistrationByProject
func (suite *ControllerTestSuite) TestGetRegistrationByProject() {
m := make(map[string]string, 1)
m[proScannerMetaKey] = "uuid"
// Configured at project level
var pid int64 = 1
suite.sample.UUID = "uuid"
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil)
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
r, err := suite.c.GetRegistrationByProject(pid)
require.NoError(suite.T(), err)
require.Equal(suite.T(), "forUT", r.Name)
// Not configured at project level, return system default
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(nil, nil)
suite.mMgr.On("GetDefault").Return(suite.sample, nil)
r, err = suite.c.GetRegistrationByProject(pid)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "forUT", r.Name)
}
// MockScannerManager is mock of the scanner manager
type MockScannerManager struct {
mock.Mock
}
// List ...
func (m *MockScannerManager) List(query *q.Query) ([]*scanner.Registration, error) {
args := m.Called(query)
return args.Get(0).([]*scanner.Registration), args.Error(1)
}
// Create ...
func (m *MockScannerManager) Create(registration *scanner.Registration) (string, error) {
args := m.Called(registration)
return args.String(0), args.Error(1)
}
// Get ...
func (m *MockScannerManager) Get(registrationUUID string) (*scanner.Registration, error) {
args := m.Called(registrationUUID)
r := args.Get(0)
if r == nil {
return nil, args.Error(1)
}
return r.(*scanner.Registration), args.Error(1)
}
// Update ...
func (m *MockScannerManager) Update(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// Delete ...
func (m *MockScannerManager) Delete(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// SetAsDefault ...
func (m *MockScannerManager) SetAsDefault(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// GetDefault ...
func (m *MockScannerManager) GetDefault() (*scanner.Registration, error) {
args := m.Called()
return args.Get(0).(*scanner.Registration), args.Error(1)
}
// MockProMetaManager is the mock of the ProjectMetadataManager
type MockProMetaManager struct {
mock.Mock
}
// Add ...
func (m *MockProMetaManager) Add(projectID int64, meta map[string]string) error {
args := m.Called(projectID, meta)
return args.Error(0)
}
// Delete ...
func (m *MockProMetaManager) Delete(projecdtID int64, meta ...string) error {
args := m.Called(projecdtID, meta)
return args.Error(0)
}
// Update ...
func (m *MockProMetaManager) Update(projectID int64, meta map[string]string) error {
args := m.Called(projectID, meta)
return args.Error(0)
}
// Get ...
func (m *MockProMetaManager) Get(projectID int64, meta ...string) (map[string]string, error) {
args := m.Called(projectID, meta)
return args.Get(0).(map[string]string), args.Error(1)
}
// List ...
func (m *MockProMetaManager) List(name, value string) ([]*models.ProjectMetadata, error) {
args := m.Called(name, value)
return args.Get(0).([]*models.ProjectMetadata), args.Error(1)
}

View File

@ -0,0 +1,194 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"github.com/goharbor/harbor/src/core/promgr/metamgr"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/q"
rscanner "github.com/goharbor/harbor/src/pkg/scan/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
"github.com/pkg/errors"
)
const (
proScannerMetaKey = "projectScanner"
)
// DefaultController is a singleton api controller for plug scanners
var DefaultController = New()
// New a basic controller
func New() Controller {
return &basicController{
manager: rscanner.New(),
proMetaMgr: metamgr.NewDefaultProjectMetadataManager(),
}
}
// basicController is default implementation of api.Controller interface
type basicController struct {
// managers for managing the scanner registrations
manager rscanner.Manager
// for operating the project level configured scanner
proMetaMgr metamgr.ProjectMetadataManager
// controller for scan actions
c scan.Controller
// Client
}
// ListRegistrations ...
func (bc *basicController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) {
return bc.manager.List(query)
}
// CreateRegistration ...
func (bc *basicController) CreateRegistration(registration *scanner.Registration) (string, error) {
// TODO: Get metadata from the adapter service first
l, err := bc.manager.List(nil)
if err != nil {
return "", err
}
if len(l) == 0 && !registration.IsDefault {
// Mark the 1st as default automatically
registration.IsDefault = true
}
return bc.manager.Create(registration)
}
// GetRegistration ...
func (bc *basicController) GetRegistration(registrationUUID string) (*scanner.Registration, error) {
return bc.manager.Get(registrationUUID)
}
// RegistrationExists ...
func (bc *basicController) RegistrationExists(registrationUUID string) bool {
registration, err := bc.manager.Get(registrationUUID)
// Just logged when an error occurred
if err != nil {
logger.Errorf("Check existence of registration error: %s", err)
}
return !(err == nil && registration == nil)
}
// UpdateRegistration ...
func (bc *basicController) UpdateRegistration(registration *scanner.Registration) error {
return bc.manager.Update(registration)
}
// SetDefaultRegistration ...
func (bc *basicController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) {
registration, err := bc.manager.Get(registrationUUID)
if registration == nil && err == nil {
// Not found
return nil, nil
}
if err := bc.manager.Delete(registrationUUID); err != nil {
return nil, errors.Wrap(err, "delete registration")
}
return registration, nil
}
// SetDefaultRegistration ...
func (bc *basicController) SetDefaultRegistration(registrationUUID string) error {
return bc.manager.SetAsDefault(registrationUUID)
}
// SetRegistrationByProject ...
func (bc *basicController) SetRegistrationByProject(projectID int64, registrationID string) error {
if projectID == 0 {
return errors.New("invalid project ID")
}
if len(registrationID) == 0 {
return errors.New("missing scanner UUID")
}
// Only keep the UUID in the metadata of the given project
// Scanner metadata existing?
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
if err != nil {
return errors.Wrap(err, "set project scanner")
}
// Update if exists
if len(m) > 0 {
// Compare and set new
if registrationID != m[proScannerMetaKey] {
m[proScannerMetaKey] = registrationID
if err := bc.proMetaMgr.Update(projectID, m); err != nil {
return errors.Wrap(err, "set project scanner")
}
}
} else {
meta := make(map[string]string, 1)
meta[proScannerMetaKey] = registrationID
if err := bc.proMetaMgr.Add(projectID, meta); err != nil {
return errors.Wrap(err, "set project scanner")
}
}
return nil
}
// GetRegistrationByProject ...
func (bc *basicController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) {
if projectID == 0 {
return nil, errors.New("invalid project ID")
}
// First, get it from the project metadata
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
if err != nil {
return nil, errors.Wrap(err, "get project scanner")
}
if len(m) > 0 {
if registrationID, ok := m[proScannerMetaKey]; ok && len(registrationID) > 0 {
registration, err := bc.manager.Get(registrationID)
if err != nil {
return nil, errors.Wrap(err, "get project scanner")
}
if registration == nil {
// Not found
// Might be deleted by the admin, the project scanner ID reference should be cleared
if err := bc.proMetaMgr.Delete(projectID, proScannerMetaKey); err != nil {
return nil, errors.Wrap(err, "get project scanner")
}
} else {
return registration, nil
}
}
}
// Second, get the default one
registration, err := bc.manager.GetDefault()
// TODO: Check status by the client later
return registration, err
}
// Ping ...
func (bc *basicController) Ping(registration *scanner.Registration) error {
return nil
}

View File

@ -0,0 +1,35 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
)
// Scan ...
func (bc *basicController) Scan(artifact *scan.Artifact) error {
return nil
}
// GetReport ...
func (bc *basicController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
return nil, nil
}
// GetScanLog ...
func (bc *basicController) GetScanLog(digest string) ([]byte, error) {
return nil, nil
}

View File

@ -0,0 +1,43 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan
import "time"
// Report of the scan
// Identified by the `digest` and `endpoint_id`
type Report struct {
ID int64 `orm:"pk;auto;column(id)"`
Digest string `orm:"column(digest)"`
ReregistrationID string `orm:"column(registration_id)"`
JobID string `orm:"column(job_id)"`
Status string `orm:"column(status)"`
StatusCode int `orm:"column(status_code)"`
Report string `orm:"column(report);type(json)"`
StartTime time.Time `orm:"column(start_time);auto_now_add;type(datetime)"`
EndTime time.Time `orm:"column(end_time);type(datetime)"`
}
// TableName for Report
func (r *Report) TableName() string {
return "scanner_report"
}
// TableUnique for Report
func (r *Report) TableUnique() [][]string {
return [][]string{
{"digest", "registration_id"},
}
}

View File

@ -0,0 +1,120 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"encoding/json"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
)
// Registration represents a named configuration for invoking a scanner via its adapter.
// UUID will be used to track the scanner.Endpoint as unique ID
type Registration struct {
// Basic information
// int64 ID is kept for being aligned with previous DB schema
ID int64 `orm:"pk;auto;column(id)" json:"-"`
UUID string `orm:"unique;column(uuid)" json:"uuid"`
Name string `orm:"unique;column(name);size(128)" json:"name"`
Description string `orm:"column(description);null;size(1024)" json:"description"`
URL string `orm:"column(url);unique;size(512)" json:"url"`
Disabled bool `orm:"column(disabled);default(true)" json:"disabled"`
IsDefault bool `orm:"column(is_default);default(false)" json:"is_default"`
Health bool `orm:"-" json:"health"`
// Authentication settings
// "None","Basic" and "Bearer" can be supported
Auth string `orm:"column(auth);size(16)" json:"auth"`
AccessCredential string `orm:"column(access_cred);null;size(512)" json:"access_credential,omitempty"`
// Http connection settings
SkipCertVerify bool `orm:"column(skip_cert_verify);default(false)" json:"skip_certVerify"`
// Adapter settings
Adapter string `orm:"column(adapter);size(128)" json:"adapter"`
Vendor string `orm:"column(vendor);size(128)" json:"vendor"`
Version string `orm:"column(version);size(32)" json:"version"`
// Timestamps
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
}
// TableName for Endpoint
func (r *Registration) TableName() string {
return "scanner_registration"
}
// FromJSON parses json data
func (r *Registration) FromJSON(jsonData string) error {
if len(jsonData) == 0 {
return errors.New("empty json data to parse")
}
return json.Unmarshal([]byte(jsonData), r)
}
// ToJSON marshals endpoint to JSON data
func (r *Registration) ToJSON() (string, error) {
data, err := json.Marshal(r)
if err != nil {
return "", err
}
return string(data), nil
}
// Validate endpoint
func (r *Registration) Validate(checkUUID bool) error {
if checkUUID && len(r.UUID) == 0 {
return errors.New("malformed endpoint")
}
if len(r.Name) == 0 {
return errors.New("missing registration name")
}
err := checkURL(r.URL)
if err != nil {
return errors.Wrap(err, "scanner registration validate")
}
if len(r.Adapter) == 0 ||
len(r.Vendor) == 0 ||
len(r.Version) == 0 {
return errors.Errorf("missing adapter settings in registration %s:%s", r.Name, r.URL)
}
return nil
}
// Check the registration URL with url package
func checkURL(u string) error {
if len(strings.TrimSpace(u)) == 0 {
return errors.New("empty url")
}
uri, err := url.Parse(u)
if err == nil {
if uri.Scheme != "http" && uri.Scheme != "https" {
err = errors.New("invalid scheme")
}
}
return err
}

View File

@ -0,0 +1,87 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ModelTestSuite tests the utility functions of the model
type ModelTestSuite struct {
suite.Suite
}
// TestModel is the entry of the model test suite
func TestModel(t *testing.T) {
suite.Run(t, new(ModelTestSuite))
}
// TestJSON tests the marshal and unmarshal functions
func (suite *ModelTestSuite) TestJSON() {
r := &Registration{
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
json, err := r.ToJSON()
require.NoError(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) {
success = len(json) > 0
return
})
r2 := &Registration{}
err = r2.FromJSON(json)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), "forUT", r2.Name)
}
// TestValidate tests the validate function
func (suite *ModelTestSuite) TestValidate() {
r := &Registration{}
err := r.Validate(true)
require.Error(suite.T(), err)
r.UUID = "uuid"
err = r.Validate(true)
require.Error(suite.T(), err)
r.Name = "forUT"
err = r.Validate(true)
require.Error(suite.T(), err)
r.URL = "a.b.c"
err = r.Validate(true)
require.Error(suite.T(), err)
r.URL = "http://a.b.c"
err = r.Validate(true)
require.Error(suite.T(), err)
r.Adapter = "Clair"
r.Vendor = "Harbor"
r.Version = "0.1.0"
err = r.Validate(true)
require.NoError(suite.T(), err)
}

View File

@ -0,0 +1,147 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"fmt"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/pkg/errors"
)
func init() {
orm.RegisterModel(new(Registration))
}
// AddRegistration adds a new registration
func AddRegistration(r *Registration) (int64, error) {
o := dao.GetOrmer()
return o.Insert(r)
}
// GetRegistration gets the specified registration
func GetRegistration(UUID string) (*Registration, error) {
e := &Registration{}
o := dao.GetOrmer()
qs := o.QueryTable(new(Registration))
if err := qs.Filter("uuid", UUID).One(e); err != nil {
if err == orm.ErrNoRows {
// Not existing case
return nil, nil
}
return nil, err
}
return e, nil
}
// UpdateRegistration update the specified registration
func UpdateRegistration(r *Registration, cols ...string) error {
o := dao.GetOrmer()
count, err := o.Update(r, cols...)
if err != nil {
return err
}
if count == 0 {
return errors.Errorf("no item with UUID %s is updated", r.UUID)
}
return nil
}
// DeleteRegistration deletes the registration with the specified UUID
func DeleteRegistration(UUID string) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
// delete with query way
count, err := qt.Filter("uuid", UUID).Delete()
if err != nil {
return err
}
if count == 0 {
return errors.Errorf("no item with UUID %s is deleted", UUID)
}
return nil
}
// ListRegistrations lists all the existing registrations
func ListRegistrations(query *q.Query) ([]*Registration, error) {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
if query != nil {
if len(query.Keywords) > 0 {
for k, v := range query.Keywords {
qt = qt.Filter(fmt.Sprintf("%s__icontains", k), v)
}
}
if query.PageNumber > 0 && query.PageSize > 0 {
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
}
l := make([]*Registration, 0)
_, err := qt.All(&l)
return l, err
}
// SetDefaultRegistration sets the specified registration as default one
func SetDefaultRegistration(UUID string) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
_, err := qt.Filter("is_default", true).Update(orm.Params{
"is_default": false,
})
if err != nil {
return err
}
qt2 := o.QueryTable(new(Registration))
_, err = qt2.Filter("uuid", UUID).Update(orm.Params{
"is_default": true,
})
return err
}
// GetDefaultRegistration gets the default registration
func GetDefaultRegistration() (*Registration, error) {
o := dao.GetOrmer()
qt := o.QueryTable(new(Registration))
e := &Registration{}
if err := qt.Filter("is_default", true).One(e); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return e, nil
}

View File

@ -0,0 +1,144 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// RegistrationDAOTestSuite is test suite of testing registration DAO
type RegistrationDAOTestSuite struct {
suite.Suite
registrationID string
}
// TestRegistrationDAO is entry of test cases
func TestRegistrationDAO(t *testing.T) {
suite.Run(t, new(RegistrationDAOTestSuite))
}
// SetupSuite prepare testing env for the suite
func (suite *RegistrationDAOTestSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
}
// SetupTest prepare stuff for test cases
func (suite *RegistrationDAOTestSuite) SetupTest() {
suite.registrationID = uuid.New().String()
r := &Registration{
UUID: suite.registrationID,
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
_, err := AddRegistration(r)
require.NoError(suite.T(), err, "add new registration")
}
// TearDownTest clears all the stuff of test cases
func (suite *RegistrationDAOTestSuite) TearDownTest() {
err := DeleteRegistration(suite.registrationID)
require.NoError(suite.T(), err, "clear registration")
}
// TestGet tests get registration
func (suite *RegistrationDAOTestSuite) TestGet() {
// Found
r, err := GetRegistration(suite.registrationID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), r.Name, "forUT")
// Not found
re, err := GetRegistration("not_found")
require.NoError(suite.T(), err)
require.Nil(suite.T(), re)
}
// TestUpdate tests update registration
func (suite *RegistrationDAOTestSuite) TestUpdate() {
r, err := GetRegistration(suite.registrationID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
r.Disabled = true
r.IsDefault = true
r.URL = "http://updated.registration.com"
err = UpdateRegistration(r)
require.NoError(suite.T(), err, "update registration")
r, err = GetRegistration(suite.registrationID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), true, r.Disabled)
assert.Equal(suite.T(), true, r.IsDefault)
assert.Equal(suite.T(), "http://updated.registration.com", r.URL)
}
// TestList tests list registrations
func (suite *RegistrationDAOTestSuite) TestList() {
// no query
l, err := ListRegistrations(nil)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
// with query and found items
keywords := make(map[string]string)
keywords["adapter"] = "Clair"
l, err = ListRegistrations(&q.Query{
PageSize: 5,
PageNumber: 1,
Keywords: keywords,
})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
// With query and not found items
keywords["adapter"] = "Micro scanner"
l, err = ListRegistrations(&q.Query{
Keywords: keywords,
})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 0, len(l))
}
// TestDefault tests set/get default
func (suite *RegistrationDAOTestSuite) TestDefault() {
dr, err := GetDefaultRegistration()
require.NoError(suite.T(), err, "not found")
require.Nil(suite.T(), dr)
err = SetDefaultRegistration(suite.registrationID)
require.NoError(suite.T(), err)
dr, err = GetDefaultRegistration()
require.NoError(suite.T(), err)
require.NotNil(suite.T(), dr)
}

View File

@ -0,0 +1,131 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/google/uuid"
"github.com/pkg/errors"
)
// Manager defines the related scanner API endpoints
type Manager interface {
// List returns a list of currently configured scanner registrations.
// Query parameters are optional
List(query *q.Query) ([]*scanner.Registration, error)
// Create creates a new scanner registration with the given data.
// Returns the scanner registration identifier.
Create(registration *scanner.Registration) (string, error)
// Get returns the details of the specified scanner registration.
Get(registrationUUID string) (*scanner.Registration, error)
// Update updates the specified scanner registration.
Update(registration *scanner.Registration) error
// Delete deletes the specified scanner registration.
Delete(registrationUUID string) error
// SetAsDefault marks the specified scanner registration as default.
// The implementation is supposed to unset any registration previously set as default.
SetAsDefault(registrationUUID string) error
// GetDefault returns the default scanner registration or `nil` if there are no registrations configured.
GetDefault() (*scanner.Registration, error)
}
// basicManager is the default implementation of Manager
type basicManager struct{}
// New a basic manager
func New() Manager {
return &basicManager{}
}
// Create ...
func (bm *basicManager) Create(registration *scanner.Registration) (string, error) {
if registration == nil {
return "", errors.New("nil endpoint to create")
}
// Inject new UUID
uid, err := uuid.NewUUID()
if err != nil {
return "", errors.Wrap(err, "new UUID: create registration")
}
registration.UUID = uid.String()
if err := registration.Validate(true); err != nil {
return "", errors.Wrap(err, "create registration")
}
if _, err := scanner.AddRegistration(registration); err != nil {
return "", errors.Wrap(err, "dao: create registration")
}
return uid.String(), nil
}
// Get ...
func (bm *basicManager) Get(registrationUUID string) (*scanner.Registration, error) {
if len(registrationUUID) == 0 {
return nil, errors.New("empty uuid of registration")
}
return scanner.GetRegistration(registrationUUID)
}
// Update ...
func (bm *basicManager) Update(registration *scanner.Registration) error {
if registration == nil {
return errors.New("nil endpoint to update")
}
if err := registration.Validate(true); err != nil {
return errors.Wrap(err, "update endpoint")
}
return scanner.UpdateRegistration(registration)
}
// Delete ...
func (bm *basicManager) Delete(registrationUUID string) error {
if len(registrationUUID) == 0 {
return errors.New("empty UUID to delete")
}
return scanner.DeleteRegistration(registrationUUID)
}
// List ...
func (bm *basicManager) List(query *q.Query) ([]*scanner.Registration, error) {
return scanner.ListRegistrations(query)
}
// SetAsDefault ...
func (bm *basicManager) SetAsDefault(registrationUUID string) error {
if len(registrationUUID) == 0 {
return errors.New("empty UUID to set default")
}
return scanner.SetDefaultRegistration(registrationUUID)
}
// GetDefault ...
func (bm *basicManager) GetDefault() (*scanner.Registration, error) {
return scanner.GetDefaultRegistration()
}

View File

@ -0,0 +1,115 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scanner
import (
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// BasicManagerTestSuite tests the basic manager
type BasicManagerTestSuite struct {
suite.Suite
mgr Manager
sampleUUID string
}
// TestBasicManager is the entry of BasicManagerTestSuite
func TestBasicManager(t *testing.T) {
suite.Run(t, new(BasicManagerTestSuite))
}
// SetupSuite prepares env for test suite
func (suite *BasicManagerTestSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
suite.mgr = New()
r := &scanner.Registration{
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
uid, err := suite.mgr.Create(r)
require.NoError(suite.T(), err)
suite.sampleUUID = uid
}
// TearDownSuite clears env for test suite
func (suite *BasicManagerTestSuite) TearDownSuite() {
err := suite.mgr.Delete(suite.sampleUUID)
require.NoError(suite.T(), err, "delete registration")
}
// TestList tests list registrations
func (suite *BasicManagerTestSuite) TestList() {
m := make(map[string]string, 1)
m["name"] = "forUT"
l, err := suite.mgr.List(&q.Query{
PageNumber: 1,
PageSize: 10,
Keywords: m,
})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
}
// TestGet tests get registration
func (suite *BasicManagerTestSuite) TestGet() {
r, err := suite.mgr.Get(suite.sampleUUID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "forUT", r.Name)
}
// TestUpdate tests update registration
func (suite *BasicManagerTestSuite) TestUpdate() {
r, err := suite.mgr.Get(suite.sampleUUID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
r.URL = "https://updated.com"
err = suite.mgr.Update(r)
require.NoError(suite.T(), err)
r, err = suite.mgr.Get(suite.sampleUUID)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), r)
assert.Equal(suite.T(), "https://updated.com", r.URL)
}
// TestDefault tests get/set default registration
func (suite *BasicManagerTestSuite) TestDefault() {
err := suite.mgr.SetAsDefault(suite.sampleUUID)
require.NoError(suite.T(), err)
dr, err := suite.mgr.GetDefault()
require.NoError(suite.T(), err)
require.NotNil(suite.T(), dr)
assert.Equal(suite.T(), true, dr.IsDefault)
}

View File

@ -0,0 +1,48 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan
import "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
// Options object for the scan action
type Options struct{}
// Option for scan action
type Option interface {
// Apply option to the passing in options
Apply(options *Options) error
}
// Controller defines operations for scan controlling
type Controller interface {
// Scan the given artifact
//
// Arguments:
// artifact *res.Artifact : artifact to be scanned
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *Artifact, options ...Option) error
// GetReport gets the reports for the given artifact identified by the digest
//
// Arguments:
// artifact *res.Artifact : the scanned artifact
//
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *Artifact) ([]*scan.Report, error)
}

View File

@ -0,0 +1,46 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan
// Artifact represents an artifact stored in Registry.
type Artifact struct {
// The full name of a Harbor repository containing the artifact, including the namespace.
// For example, `library/oracle/nosql`.
Repository string
// The artifact's digest, consisting of an algorithm and hex portion.
// For example, `sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b`,
// represents sha256 based digest.
Digest string
// The mime type of the scanned artifact
MimeType string
}
// Registry represents Registry connection settings.
type Registry struct {
// A base URL of the Docker Registry v2 API exposed by Harbor.
URL string
// An optional value of the HTTP Authorization header sent with each request to the Docker Registry v2 API.
// For example, `Bearer: JWTTOKENGOESHERE`.
Authorization string
}
// Request represents a structure that is sent to a Scanner Adapter to initiate artifact scanning.
// Conducts all the details required to pull the artifact from a Harbor registry.
type Request struct {
// Connection settings for the Docker Registry v2 API exposed by Harbor.
Registry *Registry
// Artifact to be scanned.
Artifact *Artifact
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,43 @@
font-weight: 600; font-weight: 600;
} }
.create-tooltip { .clr-form-horizontal {
top: -1; .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 { .d-f {
top: -7px; display: flex;
}
.font-size-13 {
font-size: 13px;
}
.mt-05 {
margin-bottom: 0.5rem;
} }
.title { .title {
@ -34,18 +65,14 @@
} }
} }
.width-70per { .width-90per {
width: 70%; width: 90%;
} }
.none { .none {
color: #ccc; color: #ccc;
} }
.underline {
border-bottom: 1px solid;
}
.color-0079bb { .color-0079bb {
color: #0079bb; color: #0079bb;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,8 +23,8 @@
</div> </div>
<div class="setting-wrapper flex-layout" *ngIf="isEditMode"> <div class="setting-wrapper flex-layout" *ngIf="isEditMode">
<span class="font-style">{{ labelEdit | translate }}</span> <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"> <select name="selectPolicy" id="selectPolicy" [(ngModel)]="scheduleType">
<option [value]="SCHEDULE_TYPE.NONE">{{'SCHEDULE.NONE' | translate}}</option> <option [value]="SCHEDULE_TYPE.NONE">{{'SCHEDULE.NONE' | translate}}</option>
<option [value]="SCHEDULE_TYPE.HOURLY">{{'SCHEDULE.HOURLY' | translate}}</option> <option [value]="SCHEDULE_TYPE.HOURLY">{{'SCHEDULE.HOURLY' | translate}}</option>
<option [value]="SCHEDULE_TYPE.DAILY">{{'SCHEDULE.DAILY' | translate}}</option> <option [value]="SCHEDULE_TYPE.DAILY">{{'SCHEDULE.DAILY' | translate}}</option>
@ -35,7 +35,7 @@
<span class="required" [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }}</span> <span class="required" [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }}</span>
<div [hidden]="scheduleType!==SCHEDULE_TYPE.CUSTOM" class="cron-input"> <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"> <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"> [(ngModel)]="cronString">
<span class="tooltip-content" *ngIf="dateInvalid"> <span class="tooltip-content" *ngIf="dateInvalid">
{{'TOOLTIP.CRON_REQUIRED' | translate }} {{'TOOLTIP.CRON_REQUIRED' | translate }}

View File

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

View File

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

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