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
env:
- OFFLINE=true
- node_js: 10.16.2
- language: node_js
node_js: 10.16.2
env:
- UI_UT=true
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 | | ✓ | ✓ | ✓ |
| See a list of project robots | | | ✓ | ✓ |
| Create/edit/delete project robots | | | | ✓ |
| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ |
| Create/edit/remove CVE whitelist | | | | ✓ |
| Enable/disable webhooks | | ✓ | ✓ | ✓ |
| Create/delete tag retention rules | | ✓ | ✓ | ✓ |
| Enable/disable tag retention rules | | ✓ | ✓ | ✓ |
| See project quotas | ✓ | ✓ | ✓ | ✓ |
| Edit project quotas | | | | |

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/goharbor/harbor/src/common/models"
"net/http"
"github.com/ghodss/yaml"
@ -37,6 +38,7 @@ import (
const (
yamlFileContentType = "application/x-yaml"
userSessionKey = "user"
)
// the managers/controllers used globally
@ -168,6 +170,12 @@ func (b *BaseController) WriteYamlData(object interface{}) {
_, _ = 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
func Init() error {
registerHealthCheckers()

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ package controllers
import (
"bytes"
"context"
"github.com/goharbor/harbor/src/core/api"
"html/template"
"net"
"net/http"
@ -38,11 +39,9 @@ import (
"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 ...
type CommonController struct {
beego.Controller
api.BaseController
i18n.Locale
}
@ -51,6 +50,9 @@ func (cc *CommonController) Render() error {
return nil
}
// Prepare overwrites the Prepare func in api.BaseController to ignore unnecessary steps
func (cc *CommonController) Prepare() {}
type messageDetail struct {
Hint string
URL string
@ -111,7 +113,7 @@ func (cc *CommonController) Login() {
if user == nil {
cc.CustomAbort(http.StatusUnauthorized, "")
}
cc.SetSession(userKey, *user)
cc.PopulateUserSession(*user)
}
// LogOut Habor UI

View File

@ -155,7 +155,7 @@ func (oc *OIDCController) Callback() {
oc.SendInternalServerError(err)
return
}
oc.SetSession(userKey, *u)
oc.PopulateUserSession(*u)
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"))
return
}
defer oc.DelSession(userInfoKey)
log.Debugf("User info string: %s\n", userInfoStr)
tb, ok := oc.GetSession(tokenKey).([]byte)
if !ok {
@ -223,11 +222,13 @@ func (oc *OIDCController) Onboard() {
return
}
oc.SendInternalServerError(err)
oc.DelSession(userInfoKey)
return
}
user.OIDCUserMeta = nil
oc.SetSession(userKey, user)
oc.DelSession(userInfoKey)
oc.PopulateUserSession(user)
}
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")
}
// Add routes for plugin scanner management
scannerAPI := &api.ScannerAPI{}
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
beego.Router("/api/scanners/:uid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
// Add routes for project level scanner
beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
// Error pages
beego.ErrorController(&controllers.ErrorController{})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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