mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 18:25:56 +01:00
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:
commit
f1367064fb
@ -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
78
SECURITY.md
Normal 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.
|
||||
|
||||
|
@ -43,3 +43,11 @@ The following table depicts the various user permission levels in a project.
|
||||
| Add/Remove labels of helm chart version | | ✓ | ✓ | ✓ |
|
||||
| See a list of project robots | | | ✓ | ✓ |
|
||||
| Create/edit/delete project robots | | | | ✓ |
|
||||
| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create/edit/remove CVE whitelist | | | | ✓ |
|
||||
| Enable/disable webhooks | | ✓ | ✓ | ✓ |
|
||||
| Create/delete tag retention rules | | ✓ | ✓ | ✓ |
|
||||
| Enable/disable tag retention rules | | ✓ | ✓ | ✓ |
|
||||
| See project quotas | ✓ | ✓ | ✓ | ✓ |
|
||||
| Edit project quotas | | | | |
|
||||
|
||||
|
34
make/migrations/postgresql/0011_1.10.0_schema.up.sql
Normal file
34
make/migrations/postgresql/0011_1.10.0_schema.up.sql
Normal file
@ -0,0 +1,34 @@
|
||||
/*Table for keeping the plug scanner registration*/
|
||||
CREATE TABLE scanner_registration
|
||||
(
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
uuid VARCHAR(64) UNIQUE NOT NULL,
|
||||
url VARCHAR(256) UNIQUE NOT NULL,
|
||||
name VARCHAR(128) UNIQUE NOT NULL,
|
||||
description VARCHAR(1024) NULL,
|
||||
auth VARCHAR(16) NOT NULL,
|
||||
access_cred VARCHAR(512) NULL,
|
||||
adapter VARCHAR(128) NOT NULL,
|
||||
vendor VARCHAR(128) NOT NULL,
|
||||
version VARCHAR(32) NOT NULL,
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
skip_cert_verify BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
/*Table for keeping the scanner report. The report details are stored as JSONB*/
|
||||
CREATE TABLE scanner_report
|
||||
(
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
digest VARCHAR(256) NOT NULL,
|
||||
registration_id VARCHAR(64) NOT NULL,
|
||||
job_id VARCHAR(32),
|
||||
status VARCHAR(16) NOT NULL,
|
||||
status_code INTEGER DEFAULT 0,
|
||||
report JSON,
|
||||
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
end_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(digest, registration_id)
|
||||
)
|
@ -6,6 +6,7 @@ COPY ./LICENSE /portal_src
|
||||
|
||||
WORKDIR /build_dir
|
||||
|
||||
|
||||
RUN cp -r /portal_src/* /build_dir \
|
||||
&& ls -la \
|
||||
&& apt-get update \
|
||||
@ -14,7 +15,7 @@ RUN cp -r /portal_src/* /build_dir \
|
||||
&& npm install \
|
||||
&& npm run build_lib \
|
||||
&& npm run link_lib \
|
||||
&& npm run release
|
||||
&& node --max_old_space_size=8192 'node_modules/@angular/cli/bin/ng' build --prod
|
||||
|
||||
|
||||
FROM photon:2.0
|
||||
|
@ -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()
|
||||
|
@ -206,6 +206,13 @@ func init() {
|
||||
beego.Router("/api/internal/switchquota", &InternalAPI{}, "put:SwitchQuota")
|
||||
beego.Router("/api/internal/syncquota", &InternalAPI{}, "post:SyncQuota")
|
||||
|
||||
// Add routes for plugin scanner management
|
||||
scannerAPI := &ScannerAPI{}
|
||||
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
|
||||
beego.Router("/api/scanners/:uid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
|
||||
// Add routes for project level scanner
|
||||
beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
|
||||
|
||||
// syncRegistry
|
||||
if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
|
||||
log.Fatalf("failed to sync repositories from registry: %v", err)
|
||||
|
348
src/core/api/plug_scanners.go
Normal file
348
src/core/api/plug_scanners.go
Normal file
@ -0,0 +1,348 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ScannerAPI provides the API for managing the plugin scanners
|
||||
type ScannerAPI struct {
|
||||
// The base controller to provide common utilities
|
||||
BaseController
|
||||
|
||||
// Controller for the plug scanners
|
||||
c api.Controller
|
||||
}
|
||||
|
||||
// Prepare sth. for the subsequent actions
|
||||
func (sa *ScannerAPI) Prepare() {
|
||||
// Call super prepare method
|
||||
sa.BaseController.Prepare()
|
||||
|
||||
// Check access permissions
|
||||
if !sa.SecurityCtx.IsAuthenticated() {
|
||||
sa.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
if !sa.SecurityCtx.IsSysAdmin() {
|
||||
sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default controller
|
||||
sa.c = api.DefaultController
|
||||
}
|
||||
|
||||
// Get the specified scanner
|
||||
func (sa *ScannerAPI) Get() {
|
||||
if r := sa.get(); r != nil {
|
||||
// Response to the client
|
||||
sa.Data["json"] = r
|
||||
sa.ServeJSON()
|
||||
}
|
||||
}
|
||||
|
||||
// List all the scanners
|
||||
func (sa *ScannerAPI) List() {
|
||||
p, pz, err := sa.GetPaginationParams()
|
||||
if err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: list all"))
|
||||
return
|
||||
}
|
||||
|
||||
query := &q.Query{
|
||||
PageSize: pz,
|
||||
PageNumber: p,
|
||||
}
|
||||
|
||||
// Get query key words
|
||||
kws := make(map[string]string)
|
||||
properties := []string{"name", "description", "url"}
|
||||
for _, k := range properties {
|
||||
kw := sa.GetString(k)
|
||||
if len(kw) > 0 {
|
||||
kws[k] = kw
|
||||
}
|
||||
}
|
||||
|
||||
if len(kws) > 0 {
|
||||
query.Keywords = kws
|
||||
}
|
||||
|
||||
all, err := sa.c.ListRegistrations(query)
|
||||
if err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: list all"))
|
||||
return
|
||||
}
|
||||
|
||||
// Response to the client
|
||||
sa.Data["json"] = all
|
||||
sa.ServeJSON()
|
||||
}
|
||||
|
||||
// Create a new scanner
|
||||
func (sa *ScannerAPI) Create() {
|
||||
r := &scanner.Registration{}
|
||||
|
||||
if err := sa.DecodeJSONReq(r); err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.Validate(false); err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: create"))
|
||||
return
|
||||
}
|
||||
|
||||
// Explicitly check if conflict
|
||||
if !sa.checkDuplicated("name", r.Name) ||
|
||||
!sa.checkDuplicated("url", r.URL) {
|
||||
return
|
||||
}
|
||||
|
||||
// All newly created should be non default one except the 1st one
|
||||
r.IsDefault = false
|
||||
|
||||
uuid, err := sa.c.CreateRegistration(r)
|
||||
if err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: create"))
|
||||
return
|
||||
}
|
||||
|
||||
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, uuid)
|
||||
sa.Ctx.ResponseWriter.Header().Add("Location", location)
|
||||
|
||||
resp := make(map[string]string, 1)
|
||||
resp["uuid"] = uuid
|
||||
|
||||
// Response to the client
|
||||
sa.Ctx.ResponseWriter.WriteHeader(http.StatusCreated)
|
||||
sa.Data["json"] = resp
|
||||
sa.ServeJSON()
|
||||
}
|
||||
|
||||
// Update a scanner
|
||||
func (sa *ScannerAPI) Update() {
|
||||
r := sa.get()
|
||||
if r == nil {
|
||||
// meet error
|
||||
return
|
||||
}
|
||||
|
||||
// full dose updated
|
||||
rr := &scanner.Registration{}
|
||||
if err := sa.DecodeJSONReq(rr); err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.Validate(true); err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: update"))
|
||||
return
|
||||
}
|
||||
|
||||
// Name changed?
|
||||
if r.Name != rr.Name {
|
||||
if !sa.checkDuplicated("name", rr.Name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// URL changed?
|
||||
if r.URL != rr.URL {
|
||||
if !sa.checkDuplicated("url", rr.URL) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
getChanges(r, rr)
|
||||
|
||||
if err := sa.c.UpdateRegistration(r); err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: update"))
|
||||
return
|
||||
}
|
||||
|
||||
location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, r.UUID)
|
||||
sa.Ctx.ResponseWriter.Header().Add("Location", location)
|
||||
|
||||
// Response to the client
|
||||
sa.Data["json"] = r
|
||||
sa.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete the scanner
|
||||
func (sa *ScannerAPI) Delete() {
|
||||
uid := sa.GetStringFromPath(":uid")
|
||||
if len(uid) == 0 {
|
||||
sa.SendBadRequestError(errors.New("missing uid"))
|
||||
return
|
||||
}
|
||||
|
||||
deleted, err := sa.c.DeleteRegistration(uid)
|
||||
if err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: delete"))
|
||||
return
|
||||
}
|
||||
|
||||
if deleted == nil {
|
||||
// Not found
|
||||
sa.SendNotFoundError(errors.Errorf("scanner registration: %s", uid))
|
||||
return
|
||||
}
|
||||
|
||||
sa.Data["json"] = deleted
|
||||
sa.ServeJSON()
|
||||
}
|
||||
|
||||
// SetAsDefault sets the given registration as default one
|
||||
func (sa *ScannerAPI) SetAsDefault() {
|
||||
uid := sa.GetStringFromPath(":uid")
|
||||
if len(uid) == 0 {
|
||||
sa.SendBadRequestError(errors.New("missing uid"))
|
||||
return
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
if err := sa.DecodeJSONReq(&m); err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set as default"))
|
||||
return
|
||||
}
|
||||
|
||||
if v, ok := m["is_default"]; ok {
|
||||
if isDefault, y := v.(bool); y && isDefault {
|
||||
if err := sa.c.SetDefaultRegistration(uid); err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set as default"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Not supported
|
||||
sa.SendForbiddenError(errors.Errorf("not supported: %#v", m))
|
||||
}
|
||||
|
||||
// GetProjectScanner gets the project level scanner
|
||||
func (sa *ScannerAPI) GetProjectScanner() {
|
||||
pid, err := sa.GetInt64FromPath(":pid")
|
||||
if err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: get project scanners"))
|
||||
return
|
||||
}
|
||||
|
||||
r, err := sa.c.GetRegistrationByProject(pid)
|
||||
if err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get project scanners"))
|
||||
return
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
sa.Data["json"] = r
|
||||
} else {
|
||||
sa.Data["json"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
sa.ServeJSON()
|
||||
}
|
||||
|
||||
// SetProjectScanner sets the project level scanner
|
||||
func (sa *ScannerAPI) SetProjectScanner() {
|
||||
pid, err := sa.GetInt64FromPath(":pid")
|
||||
if err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners"))
|
||||
return
|
||||
}
|
||||
|
||||
body := make(map[string]string)
|
||||
if err := sa.DecodeJSONReq(&body); err != nil {
|
||||
sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners"))
|
||||
return
|
||||
}
|
||||
|
||||
uuid, ok := body["uuid"]
|
||||
if !ok || len(uuid) == 0 {
|
||||
sa.SendBadRequestError(errors.New("missing scanner uuid when setting project scanner"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := sa.c.SetRegistrationByProject(pid, uuid); err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: set project scanners"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// get the specified scanner
|
||||
func (sa *ScannerAPI) get() *scanner.Registration {
|
||||
uid := sa.GetStringFromPath(":uid")
|
||||
if len(uid) == 0 {
|
||||
sa.SendBadRequestError(errors.New("missing uid"))
|
||||
return nil
|
||||
}
|
||||
|
||||
r, err := sa.c.GetRegistration(uid)
|
||||
if err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: get"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
// NOT found
|
||||
sa.SendNotFoundError(errors.Errorf("scanner: %s", uid))
|
||||
return nil
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (sa *ScannerAPI) checkDuplicated(property, value string) bool {
|
||||
// Explicitly check if conflict
|
||||
kw := make(map[string]string)
|
||||
kw[property] = value
|
||||
|
||||
query := &q.Query{
|
||||
Keywords: kw,
|
||||
}
|
||||
|
||||
l, err := sa.c.ListRegistrations(query)
|
||||
if err != nil {
|
||||
sa.SendInternalServerError(errors.Wrap(err, "scanner API: check existence"))
|
||||
return false
|
||||
}
|
||||
|
||||
if len(l) > 0 {
|
||||
sa.SendConflictError(errors.Errorf("duplicated entries: %s:%s", property, value))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getChanges(e *scanner.Registration, eChange *scanner.Registration) {
|
||||
e.Name = eChange.Name
|
||||
e.Description = eChange.Description
|
||||
e.URL = eChange.URL
|
||||
e.Auth = eChange.Auth
|
||||
e.AccessCredential = eChange.AccessCredential
|
||||
e.Disabled = eChange.Disabled
|
||||
e.SkipCertVerify = eChange.SkipCertVerify
|
||||
}
|
444
src/core/api/plug_scanners_test.go
Normal file
444
src/core/api/plug_scanners_test.go
Normal file
@ -0,0 +1,444 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
|
||||
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
const (
|
||||
rootRoute = "/api/scanners"
|
||||
)
|
||||
|
||||
// ScannerAPITestSuite is test suite for testing the scanner API
|
||||
type ScannerAPITestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
originC api.Controller
|
||||
mockC *MockScannerAPIController
|
||||
}
|
||||
|
||||
// TestScannerAPI is the entry of ScannerAPITestSuite
|
||||
func TestScannerAPI(t *testing.T) {
|
||||
suite.Run(t, new(ScannerAPITestSuite))
|
||||
}
|
||||
|
||||
// SetupSuite prepares testing env
|
||||
func (suite *ScannerAPITestSuite) SetupTest() {
|
||||
suite.originC = api.DefaultController
|
||||
m := &MockScannerAPIController{}
|
||||
api.DefaultController = m
|
||||
|
||||
suite.mockC = m
|
||||
}
|
||||
|
||||
// TearDownTest clears test case env
|
||||
func (suite *ScannerAPITestSuite) TearDownTest() {
|
||||
// Restore
|
||||
api.DefaultController = suite.originC
|
||||
}
|
||||
|
||||
// TestScannerAPICreate tests the post request to create new one
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPIBase() {
|
||||
// Including general cases
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: rootRoute,
|
||||
method: http.MethodPost,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: rootRoute,
|
||||
method: http.MethodPost,
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
url: rootRoute,
|
||||
method: http.MethodPost,
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &scanner.Registration{
|
||||
URL: "http://a.b.c",
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(suite.T(), cases...)
|
||||
}
|
||||
|
||||
// TestScannerAPIGet tests api get
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPIGet() {
|
||||
res := &scanner.Registration{
|
||||
ID: 1000,
|
||||
UUID: "uuid",
|
||||
Name: "TestScannerAPIGet",
|
||||
Description: "JUST FOR TEST",
|
||||
URL: "https://a.b.c",
|
||||
Adapter: "Clair",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
}
|
||||
suite.mockC.On("GetRegistration", "uuid").Return(res, nil)
|
||||
|
||||
// Get
|
||||
rr := &scanner.Registration{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
|
||||
method: http.MethodGet,
|
||||
credential: sysAdmin,
|
||||
}, rr)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), rr)
|
||||
assert.Equal(suite.T(), res.Name, rr.Name)
|
||||
assert.Equal(suite.T(), res.UUID, rr.UUID)
|
||||
}
|
||||
|
||||
// TestScannerAPICreate tests create.
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPICreate() {
|
||||
r := &scanner.Registration{
|
||||
Name: "TestScannerAPICreate",
|
||||
Description: "JUST FOR TEST",
|
||||
URL: "https://a.b.c",
|
||||
Adapter: "Clair",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
}
|
||||
|
||||
suite.mockQuery(r)
|
||||
suite.mockC.On("CreateRegistration", r).Return("uuid", nil)
|
||||
|
||||
// Create
|
||||
res := make(map[string]string, 1)
|
||||
err := handleAndParse(
|
||||
&testingRequest{
|
||||
url: rootRoute,
|
||||
method: http.MethodPost,
|
||||
credential: sysAdmin,
|
||||
bodyJSON: r,
|
||||
}, &res)
|
||||
require.NoError(suite.T(), err)
|
||||
require.Condition(suite.T(), func() (success bool) {
|
||||
success = res["uuid"] == "uuid"
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// TestScannerAPIList tests list
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPIList() {
|
||||
query := &q.Query{
|
||||
PageNumber: 1,
|
||||
PageSize: 500,
|
||||
}
|
||||
ll := []*scanner.Registration{
|
||||
{
|
||||
ID: 1001,
|
||||
UUID: "uuid",
|
||||
Name: "TestScannerAPIList",
|
||||
Description: "JUST FOR TEST",
|
||||
URL: "https://a.b.c",
|
||||
Adapter: "Clair",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
}}
|
||||
suite.mockC.On("ListRegistrations", query).Return(ll, nil)
|
||||
|
||||
// List
|
||||
l := make([]*scanner.Registration, 0)
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: rootRoute,
|
||||
method: http.MethodGet,
|
||||
credential: sysAdmin,
|
||||
}, &l)
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Condition(suite.T(), func() (success bool) {
|
||||
success = len(l) > 0 && l[0].Name == ll[0].Name
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// TestScannerAPIUpdate tests the update API
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPIUpdate() {
|
||||
before := &scanner.Registration{
|
||||
ID: 1002,
|
||||
UUID: "uuid",
|
||||
Name: "TestScannerAPIUpdate_before",
|
||||
Description: "JUST FOR TEST",
|
||||
URL: "https://a.b.c",
|
||||
Adapter: "Clair",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
}
|
||||
|
||||
updated := &scanner.Registration{
|
||||
ID: 1002,
|
||||
UUID: "uuid",
|
||||
Name: "TestScannerAPIUpdate",
|
||||
Description: "JUST FOR TEST",
|
||||
URL: "https://a.b.c",
|
||||
Adapter: "Clair",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
}
|
||||
|
||||
suite.mockQuery(updated)
|
||||
suite.mockC.On("UpdateRegistration", updated).Return(nil)
|
||||
suite.mockC.On("GetRegistration", "uuid").Return(before, nil)
|
||||
|
||||
rr := &scanner.Registration{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
|
||||
method: http.MethodPut,
|
||||
credential: sysAdmin,
|
||||
bodyJSON: updated,
|
||||
}, rr)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), rr)
|
||||
|
||||
assert.Equal(suite.T(), updated.Name, rr.Name)
|
||||
assert.Equal(suite.T(), updated.UUID, rr.UUID)
|
||||
}
|
||||
|
||||
//
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPIDelete() {
|
||||
r := &scanner.Registration{
|
||||
ID: 1003,
|
||||
UUID: "uuid",
|
||||
Name: "TestScannerAPIDelete",
|
||||
Description: "JUST FOR TEST",
|
||||
URL: "https://a.b.c",
|
||||
Adapter: "Clair",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
}
|
||||
|
||||
suite.mockC.On("DeleteRegistration", "uuid").Return(r, nil)
|
||||
|
||||
deleted := &scanner.Registration{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
|
||||
method: http.MethodDelete,
|
||||
credential: sysAdmin,
|
||||
}, deleted)
|
||||
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), r.UUID, deleted.UUID)
|
||||
assert.Equal(suite.T(), r.Name, deleted.Name)
|
||||
}
|
||||
|
||||
// TestScannerAPISetDefault tests the set default
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPISetDefault() {
|
||||
suite.mockC.On("SetDefaultRegistration", "uuid").Return(nil)
|
||||
|
||||
body := make(map[string]interface{}, 1)
|
||||
body["is_default"] = true
|
||||
runCodeCheckingCases(suite.T(), &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s", rootRoute, "uuid"),
|
||||
method: http.MethodPatch,
|
||||
credential: sysAdmin,
|
||||
bodyJSON: body,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// TestScannerAPIProjectScanner tests the API of getting/setting project level scanner
|
||||
func (suite *ScannerAPITestSuite) TestScannerAPIProjectScanner() {
|
||||
suite.mockC.On("SetRegistrationByProject", int64(1), "uuid").Return(nil)
|
||||
|
||||
// Set
|
||||
body := make(map[string]interface{}, 1)
|
||||
body["uuid"] = "uuid"
|
||||
runCodeCheckingCases(suite.T(), &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
|
||||
method: http.MethodPut,
|
||||
credential: sysAdmin,
|
||||
bodyJSON: body,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
|
||||
r := &scanner.Registration{
|
||||
ID: 1004,
|
||||
UUID: "uuid",
|
||||
Name: "TestScannerAPIProjectScanner",
|
||||
Description: "JUST FOR TEST",
|
||||
URL: "https://a.b.c",
|
||||
Adapter: "Clair",
|
||||
Vendor: "Harbor",
|
||||
Version: "0.1.0",
|
||||
}
|
||||
suite.mockC.On("GetRegistrationByProject", int64(1)).Return(r, nil)
|
||||
|
||||
// Get
|
||||
rr := &scanner.Registration{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("/api/projects/%d/scanner", 1),
|
||||
method: http.MethodGet,
|
||||
credential: sysAdmin,
|
||||
}, rr)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
assert.Equal(suite.T(), r.Name, rr.Name)
|
||||
assert.Equal(suite.T(), r.UUID, rr.UUID)
|
||||
}
|
||||
|
||||
func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
|
||||
kw := make(map[string]string, 1)
|
||||
kw["name"] = r.Name
|
||||
query := &q.Query{
|
||||
Keywords: kw,
|
||||
}
|
||||
emptyL := make([]*scanner.Registration, 0)
|
||||
suite.mockC.On("ListRegistrations", query).Return(emptyL, nil)
|
||||
|
||||
kw2 := make(map[string]string, 1)
|
||||
kw2["url"] = r.URL
|
||||
query2 := &q.Query{
|
||||
Keywords: kw2,
|
||||
}
|
||||
suite.mockC.On("ListRegistrations", query2).Return(emptyL, nil)
|
||||
}
|
||||
|
||||
// MockScannerAPIController is mock of scanner API controller
|
||||
type MockScannerAPIController struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ListRegistrations ...
|
||||
func (m *MockScannerAPIController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) {
|
||||
args := m.Called(query)
|
||||
return args.Get(0).([]*scanner.Registration), args.Error(1)
|
||||
}
|
||||
|
||||
// CreateRegistration ...
|
||||
func (m *MockScannerAPIController) CreateRegistration(registration *scanner.Registration) (string, error) {
|
||||
args := m.Called(registration)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
// GetRegistration ...
|
||||
func (m *MockScannerAPIController) GetRegistration(registrationUUID string) (*scanner.Registration, error) {
|
||||
args := m.Called(registrationUUID)
|
||||
s := args.Get(0)
|
||||
if s == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return s.(*scanner.Registration), args.Error(1)
|
||||
}
|
||||
|
||||
// RegistrationExists ...
|
||||
func (m *MockScannerAPIController) RegistrationExists(registrationUUID string) bool {
|
||||
args := m.Called(registrationUUID)
|
||||
return args.Bool(0)
|
||||
}
|
||||
|
||||
// UpdateRegistration ...
|
||||
func (m *MockScannerAPIController) UpdateRegistration(registration *scanner.Registration) error {
|
||||
args := m.Called(registration)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// DeleteRegistration ...
|
||||
func (m *MockScannerAPIController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) {
|
||||
args := m.Called(registrationUUID)
|
||||
s := args.Get(0)
|
||||
if s == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return s.(*scanner.Registration), args.Error(1)
|
||||
}
|
||||
|
||||
// SetDefaultRegistration ...
|
||||
func (m *MockScannerAPIController) SetDefaultRegistration(registrationUUID string) error {
|
||||
args := m.Called(registrationUUID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// SetRegistrationByProject ...
|
||||
func (m *MockScannerAPIController) SetRegistrationByProject(projectID int64, scannerID string) error {
|
||||
args := m.Called(projectID, scannerID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// GetRegistrationByProject ...
|
||||
func (m *MockScannerAPIController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) {
|
||||
args := m.Called(projectID)
|
||||
s := args.Get(0)
|
||||
if s == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return s.(*scanner.Registration), args.Error(1)
|
||||
}
|
||||
|
||||
// Ping ...
|
||||
func (m *MockScannerAPIController) Ping(registration *scanner.Registration) error {
|
||||
args := m.Called(registration)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Scan ...
|
||||
func (m *MockScannerAPIController) Scan(artifact *scan.Artifact) error {
|
||||
args := m.Called(artifact)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// GetReport ...
|
||||
func (m *MockScannerAPIController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
|
||||
args := m.Called(artifact)
|
||||
r := args.Get(0)
|
||||
if r == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return r.([]*dscan.Report), args.Error(1)
|
||||
}
|
||||
|
||||
// GetScanLog ...
|
||||
func (m *MockScannerAPIController) GetScanLog(digest string) ([]byte, error) {
|
||||
args := m.Called(digest)
|
||||
l := args.Get(0)
|
||||
if l == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return l.([]byte), args.Error(1)
|
||||
}
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -192,6 +192,13 @@ func initRouters() {
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
|
||||
}
|
||||
|
||||
// Add routes for plugin scanner management
|
||||
scannerAPI := &api.ScannerAPI{}
|
||||
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
|
||||
beego.Router("/api/scanners/:uid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
|
||||
// Add routes for project level scanner
|
||||
beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
|
||||
|
||||
// Error pages
|
||||
beego.ErrorController(&controllers.ErrorController{})
|
||||
|
||||
|
@ -45,6 +45,7 @@ require (
|
||||
github.com/google/certificate-transparency-go v1.0.21 // indirect
|
||||
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gorilla/handlers v1.3.0
|
||||
github.com/gorilla/mux v1.6.2
|
||||
github.com/graph-gophers/dataloader v5.0.0+incompatible
|
||||
|
@ -140,6 +140,8 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeq
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
|
@ -88,10 +88,10 @@ func (f *fakedTransfer) Transfer(src *model.Resource, dst *model.Resource) error
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
err := transfer.RegisterFactory("res", fakedTransferFactory)
|
||||
err := transfer.RegisterFactory("art", fakedTransferFactory)
|
||||
require.Nil(t, err)
|
||||
params := map[string]interface{}{
|
||||
"src_resource": `{"type":"res"}`,
|
||||
"src_resource": `{"type":"art"}`,
|
||||
"dst_resource": `{}`,
|
||||
}
|
||||
rep := &Replication{}
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package res
|
||||
package art
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package res
|
||||
package art
|
||||
|
||||
// Result keeps the action result
|
||||
type Result struct {
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package res
|
||||
package art
|
||||
|
||||
// Selector is used to filter the inputting list
|
||||
type Selector interface {
|
@ -16,7 +16,7 @@ package doublestar
|
||||
|
||||
import (
|
||||
"github.com/bmatcuk/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -46,7 +46,7 @@ type selector struct {
|
||||
}
|
||||
|
||||
// Select candidates by regular expressions
|
||||
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
|
||||
func (s *selector) Select(artifacts []*art.Candidate) (selected []*art.Candidate, err error) {
|
||||
value := ""
|
||||
excludes := false
|
||||
|
||||
@ -86,7 +86,7 @@ func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate
|
||||
}
|
||||
|
||||
// New is factory method for doublestar selector
|
||||
func New(decoration string, pattern string) res.Selector {
|
||||
func New(decoration string, pattern string) art.Selector {
|
||||
return &selector{
|
||||
decoration: decoration,
|
||||
pattern: pattern,
|
@ -16,7 +16,7 @@ package doublestar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -28,7 +28,7 @@ import (
|
||||
type RegExpSelectorTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
artifacts []*res.Candidate
|
||||
artifacts []*art.Candidate
|
||||
}
|
||||
|
||||
// TestRegExpSelector is entrance for RegExpSelectorTestSuite
|
||||
@ -38,13 +38,13 @@ func TestRegExpSelector(t *testing.T) {
|
||||
|
||||
// SetupSuite to do preparation work
|
||||
func (suite *RegExpSelectorTestSuite) SetupSuite() {
|
||||
suite.artifacts = []*res.Candidate{
|
||||
suite.artifacts = []*art.Candidate{
|
||||
{
|
||||
NamespaceID: 1,
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
Tag: "latest",
|
||||
Kind: res.Image,
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
@ -55,7 +55,7 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
|
||||
Namespace: "retention",
|
||||
Repository: "redis",
|
||||
Tag: "4.0",
|
||||
Kind: res.Image,
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
@ -66,7 +66,7 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
|
||||
Namespace: "retention",
|
||||
Repository: "redis",
|
||||
Tag: "4.1",
|
||||
Kind: res.Image,
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
@ -235,7 +235,7 @@ func (suite *RegExpSelectorTestSuite) TestNSExcludes() {
|
||||
}
|
||||
|
||||
// Check whether the returned result matched the expected ones (only check repo:tag)
|
||||
func expect(expected []string, candidates []*res.Candidate) bool {
|
||||
func expect(expected []string, candidates []*art.Candidate) bool {
|
||||
hash := make(map[string]bool)
|
||||
|
||||
for _, art := range candidates {
|
@ -17,8 +17,8 @@ package index
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -49,11 +49,11 @@ type IndexedMeta struct {
|
||||
// indexedItem defined item kept in the index
|
||||
type indexedItem struct {
|
||||
Meta *IndexedMeta
|
||||
Factory res.SelectorFactory
|
||||
Factory art.SelectorFactory
|
||||
}
|
||||
|
||||
// Register the selector with the corresponding selector kind and decoration
|
||||
func Register(kind string, decorations []string, factory res.SelectorFactory) {
|
||||
func Register(kind string, decorations []string, factory art.SelectorFactory) {
|
||||
if len(kind) == 0 || factory == nil {
|
||||
// do nothing
|
||||
return
|
||||
@ -69,7 +69,7 @@ func Register(kind string, decorations []string, factory res.SelectorFactory) {
|
||||
}
|
||||
|
||||
// Get selector with the provided kind and decoration
|
||||
func Get(kind, decoration, pattern string) (res.Selector, error) {
|
||||
func Get(kind, decoration, pattern string) (art.Selector, error) {
|
||||
if len(kind) == 0 || len(decoration) == 0 {
|
||||
return nil, errors.New("empty selector kind or decoration")
|
||||
}
|
@ -17,7 +17,7 @@ package label
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -39,7 +39,7 @@ type selector struct {
|
||||
}
|
||||
|
||||
// Select candidates by the labels
|
||||
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
|
||||
func (s *selector) Select(artifacts []*art.Candidate) (selected []*art.Candidate, err error) {
|
||||
for _, art := range artifacts {
|
||||
if isMatched(s.labels, art.Labels, s.decoration) {
|
||||
selected = append(selected, art)
|
||||
@ -50,7 +50,7 @@ func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate
|
||||
}
|
||||
|
||||
// New is factory method for list selector
|
||||
func New(decoration string, pattern string) res.Selector {
|
||||
func New(decoration string, pattern string) art.Selector {
|
||||
labels := make([]string, 0)
|
||||
if len(pattern) > 0 {
|
||||
labels = append(labels, strings.Split(pattern, ",")...)
|
@ -16,7 +16,7 @@ package label
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -28,7 +28,7 @@ import (
|
||||
type LabelSelectorTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
artifacts []*res.Candidate
|
||||
artifacts []*art.Candidate
|
||||
}
|
||||
|
||||
// TestLabelSelector is entrance for LabelSelectorTestSuite
|
||||
@ -38,13 +38,13 @@ func TestLabelSelector(t *testing.T) {
|
||||
|
||||
// SetupSuite to do preparation work
|
||||
func (suite *LabelSelectorTestSuite) SetupSuite() {
|
||||
suite.artifacts = []*res.Candidate{
|
||||
suite.artifacts = []*art.Candidate{
|
||||
{
|
||||
NamespaceID: 1,
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
Tag: "1.9",
|
||||
Kind: res.Image,
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
@ -55,7 +55,7 @@ func (suite *LabelSelectorTestSuite) SetupSuite() {
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
Tag: "dev",
|
||||
Kind: res.Image,
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
@ -131,7 +131,7 @@ func (suite *LabelSelectorTestSuite) TestWithoutNoneExistingLabels() {
|
||||
}
|
||||
|
||||
// Check whether the returned result matched the expected ones (only check repo:tag)
|
||||
func expect(expected []string, candidates []*res.Candidate) bool {
|
||||
func expect(expected []string, candidates []*art.Candidate) bool {
|
||||
hash := make(map[string]bool)
|
||||
|
||||
for _, art := range candidates {
|
25
src/pkg/q/query.go
Normal file
25
src/pkg/q/query.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package q
|
||||
|
||||
// Query parameters
|
||||
type Query struct {
|
||||
// Page number
|
||||
PageNumber int64
|
||||
// Page size
|
||||
PageSize int64
|
||||
// List of key words
|
||||
Keywords map[string]string
|
||||
}
|
@ -21,8 +21,8 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common/http/modifier/auth"
|
||||
"github.com/goharbor/harbor/src/jobservice/config"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/clients/core"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
// DefaultClient for the retention
|
||||
@ -33,30 +33,30 @@ type Client interface {
|
||||
// Get the tag candidates under the repository
|
||||
//
|
||||
// Arguments:
|
||||
// repo *res.Repository : repository info
|
||||
// repo *art.Repository : repository info
|
||||
//
|
||||
// Returns:
|
||||
// []*res.Candidate : candidates returned
|
||||
// []*art.Candidate : candidates returned
|
||||
// error : common error if any errors occurred
|
||||
GetCandidates(repo *res.Repository) ([]*res.Candidate, error)
|
||||
GetCandidates(repo *art.Repository) ([]*art.Candidate, error)
|
||||
|
||||
// Delete the given repository
|
||||
//
|
||||
// Arguments:
|
||||
// repo *res.Repository : repository info
|
||||
// repo *art.Repository : repository info
|
||||
//
|
||||
// Returns:
|
||||
// error : common error if any errors occurred
|
||||
DeleteRepository(repo *res.Repository) error
|
||||
DeleteRepository(repo *art.Repository) error
|
||||
|
||||
// Delete the specified candidate
|
||||
//
|
||||
// Arguments:
|
||||
// candidate *res.Candidate : the deleting candidate
|
||||
// candidate *art.Candidate : the deleting candidate
|
||||
//
|
||||
// Returns:
|
||||
// error : common error if any errors occurred
|
||||
Delete(candidate *res.Candidate) error
|
||||
Delete(candidate *art.Candidate) error
|
||||
}
|
||||
|
||||
// NewClient new a basic client
|
||||
@ -88,13 +88,13 @@ type basicClient struct {
|
||||
}
|
||||
|
||||
// GetCandidates gets the tag candidates under the repository
|
||||
func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candidate, error) {
|
||||
func (bc *basicClient) GetCandidates(repository *art.Repository) ([]*art.Candidate, error) {
|
||||
if repository == nil {
|
||||
return nil, errors.New("repository is nil")
|
||||
}
|
||||
candidates := make([]*res.Candidate, 0)
|
||||
candidates := make([]*art.Candidate, 0)
|
||||
switch repository.Kind {
|
||||
case res.Image:
|
||||
case art.Image:
|
||||
images, err := bc.coreClient.ListAllImages(repository.Namespace, repository.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -104,8 +104,8 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
|
||||
for _, label := range image.Labels {
|
||||
labels = append(labels, label.Name)
|
||||
}
|
||||
candidate := &res.Candidate{
|
||||
Kind: res.Image,
|
||||
candidate := &art.Candidate{
|
||||
Kind: art.Image,
|
||||
Namespace: repository.Namespace,
|
||||
Repository: repository.Name,
|
||||
Tag: image.Name,
|
||||
@ -118,7 +118,7 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
/*
|
||||
case res.Chart:
|
||||
case art.Chart:
|
||||
charts, err := bc.coreClient.ListAllCharts(repository.Namespace, repository.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -128,8 +128,8 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
|
||||
for _, label := range chart.Labels {
|
||||
labels = append(labels, label.Name)
|
||||
}
|
||||
candidate := &res.Candidate{
|
||||
Kind: res.Chart,
|
||||
candidate := &art.Candidate{
|
||||
Kind: art.Chart,
|
||||
Namespace: repository.Namespace,
|
||||
Repository: repository.Name,
|
||||
Tag: chart.Name,
|
||||
@ -148,15 +148,15 @@ func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candida
|
||||
}
|
||||
|
||||
// DeleteRepository deletes the specified repository
|
||||
func (bc *basicClient) DeleteRepository(repo *res.Repository) error {
|
||||
func (bc *basicClient) DeleteRepository(repo *art.Repository) error {
|
||||
if repo == nil {
|
||||
return errors.New("repository is nil")
|
||||
}
|
||||
switch repo.Kind {
|
||||
case res.Image:
|
||||
case art.Image:
|
||||
return bc.coreClient.DeleteImageRepository(repo.Namespace, repo.Name)
|
||||
/*
|
||||
case res.Chart:
|
||||
case art.Chart:
|
||||
return bc.coreClient.DeleteChartRepository(repo.Namespace, repo.Name)
|
||||
*/
|
||||
default:
|
||||
@ -165,15 +165,15 @@ func (bc *basicClient) DeleteRepository(repo *res.Repository) error {
|
||||
}
|
||||
|
||||
// Deletes the specified candidate
|
||||
func (bc *basicClient) Delete(candidate *res.Candidate) error {
|
||||
func (bc *basicClient) Delete(candidate *art.Candidate) error {
|
||||
if candidate == nil {
|
||||
return errors.New("candidate is nil")
|
||||
}
|
||||
switch candidate.Kind {
|
||||
case res.Image:
|
||||
case art.Image:
|
||||
return bc.coreClient.DeleteImage(candidate.Namespace, candidate.Repository, candidate.Tag)
|
||||
/*
|
||||
case res.Chart:
|
||||
case art.Chart:
|
||||
return bc.coreClient.DeleteChart(candidate.Namespace, candidate.Repository, candidate.Tag)
|
||||
*/
|
||||
default:
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
jmodels "github.com/goharbor/harbor/src/common/job/models"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/testing/clients"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -73,33 +73,33 @@ type clientTestSuite struct {
|
||||
func (c *clientTestSuite) TestGetCandidates() {
|
||||
client := &basicClient{}
|
||||
client.coreClient = &fakeCoreClient{}
|
||||
var repository *res.Repository
|
||||
var repository *art.Repository
|
||||
// nil repository
|
||||
candidates, err := client.GetCandidates(repository)
|
||||
require.NotNil(c.T(), err)
|
||||
|
||||
// image repository
|
||||
repository = &res.Repository{}
|
||||
repository.Kind = res.Image
|
||||
repository = &art.Repository{}
|
||||
repository.Kind = art.Image
|
||||
repository.Namespace = "library"
|
||||
repository.Name = "hello-world"
|
||||
candidates, err = client.GetCandidates(repository)
|
||||
require.Nil(c.T(), err)
|
||||
assert.Equal(c.T(), 1, len(candidates))
|
||||
assert.Equal(c.T(), res.Image, candidates[0].Kind)
|
||||
assert.Equal(c.T(), art.Image, candidates[0].Kind)
|
||||
assert.Equal(c.T(), "library", candidates[0].Namespace)
|
||||
assert.Equal(c.T(), "hello-world", candidates[0].Repository)
|
||||
assert.Equal(c.T(), "latest", candidates[0].Tag)
|
||||
|
||||
/*
|
||||
// chart repository
|
||||
repository.Kind = res.Chart
|
||||
repository.Kind = art.Chart
|
||||
repository.Namespace = "goharbor"
|
||||
repository.Name = "harbor"
|
||||
candidates, err = client.GetCandidates(repository)
|
||||
require.Nil(c.T(), err)
|
||||
assert.Equal(c.T(), 1, len(candidates))
|
||||
assert.Equal(c.T(), res.Chart, candidates[0].Kind)
|
||||
assert.Equal(c.T(), art.Chart, candidates[0].Kind)
|
||||
assert.Equal(c.T(), "goharbor", candidates[0].Namespace)
|
||||
assert.Equal(c.T(), "1.0", candidates[0].Tag)
|
||||
*/
|
||||
@ -109,20 +109,20 @@ func (c *clientTestSuite) TestDelete() {
|
||||
client := &basicClient{}
|
||||
client.coreClient = &fakeCoreClient{}
|
||||
|
||||
var candidate *res.Candidate
|
||||
var candidate *art.Candidate
|
||||
// nil candidate
|
||||
err := client.Delete(candidate)
|
||||
require.NotNil(c.T(), err)
|
||||
|
||||
// image
|
||||
candidate = &res.Candidate{}
|
||||
candidate.Kind = res.Image
|
||||
candidate = &art.Candidate{}
|
||||
candidate.Kind = art.Image
|
||||
err = client.Delete(candidate)
|
||||
require.Nil(c.T(), err)
|
||||
|
||||
/*
|
||||
// chart
|
||||
candidate.Kind = res.Chart
|
||||
candidate.Kind = art.Chart
|
||||
err = client.Delete(candidate)
|
||||
require.Nil(c.T(), err)
|
||||
*/
|
||||
|
@ -23,10 +23,10 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/dep"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -116,7 +116,7 @@ func (pj *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
return saveRetainNum(ctx, results, allCandidates)
|
||||
}
|
||||
|
||||
func saveRetainNum(ctx job.Context, retained []*res.Result, allCandidates []*res.Candidate) error {
|
||||
func saveRetainNum(ctx job.Context, retained []*art.Result, allCandidates []*art.Candidate) error {
|
||||
var delNum int
|
||||
for _, r := range retained {
|
||||
if r.Error == nil {
|
||||
@ -138,7 +138,7 @@ func saveRetainNum(ctx job.Context, retained []*res.Result, allCandidates []*res
|
||||
return nil
|
||||
}
|
||||
|
||||
func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Result) {
|
||||
func logResults(logger logger.Interface, all []*art.Candidate, results []*art.Result) {
|
||||
hash := make(map[string]error, len(results))
|
||||
for _, r := range results {
|
||||
if r.Target != nil {
|
||||
@ -146,7 +146,7 @@ func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Re
|
||||
}
|
||||
}
|
||||
|
||||
op := func(art *res.Candidate) string {
|
||||
op := func(art *art.Candidate) string {
|
||||
if e, exists := hash[art.Hash()]; exists {
|
||||
if e != nil {
|
||||
return actionMarkError
|
||||
@ -194,7 +194,7 @@ func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Re
|
||||
}
|
||||
}
|
||||
|
||||
func arn(art *res.Candidate) string {
|
||||
func arn(art *art.Candidate) string {
|
||||
return fmt.Sprintf("%s/%s:%s", art.Namespace, art.Repository, art.Tag)
|
||||
}
|
||||
|
||||
@ -237,7 +237,7 @@ func getParamDryRun(params job.Parameters) (bool, error) {
|
||||
return dryRun, nil
|
||||
}
|
||||
|
||||
func getParamRepo(params job.Parameters) (*res.Repository, error) {
|
||||
func getParamRepo(params job.Parameters) (*art.Repository, error) {
|
||||
v, ok := params[ParamRepo]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("missing parameter: %s", ParamRepo)
|
||||
@ -248,7 +248,7 @@ func getParamRepo(params job.Parameters) (*res.Repository, error) {
|
||||
return nil, errors.Errorf("invalid parameter: %s", ParamRepo)
|
||||
}
|
||||
|
||||
repo := &res.Repository{}
|
||||
repo := &art.Repository{}
|
||||
if err := repo.FromJSON(repoJSON); err != nil {
|
||||
return nil, errors.Wrap(err, "parse repository from JSON")
|
||||
}
|
||||
|
@ -22,14 +22,14 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/dep"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestps"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -60,10 +60,10 @@ func (suite *JobTestSuite) TearDownSuite() {
|
||||
func (suite *JobTestSuite) TestRunSuccess() {
|
||||
params := make(job.Parameters)
|
||||
params[ParamDryRun] = false
|
||||
repository := &res.Repository{
|
||||
repository := &art.Repository{
|
||||
Namespace: "library",
|
||||
Name: "harbor",
|
||||
Kind: res.Image,
|
||||
Kind: art.Image,
|
||||
}
|
||||
repoJSON, err := repository.ToJSON()
|
||||
require.Nil(suite.T(), err)
|
||||
@ -112,8 +112,8 @@ func (suite *JobTestSuite) TestRunSuccess() {
|
||||
type fakeRetentionClient struct{}
|
||||
|
||||
// GetCandidates ...
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
|
||||
return []*res.Candidate{
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
|
||||
return []*art.Candidate{
|
||||
{
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
@ -140,12 +140,12 @@ func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Cand
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
|
||||
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitTask ...
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/index"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/index"
|
||||
|
||||
cjob "github.com/goharbor/harbor/src/common/job"
|
||||
"github.com/goharbor/harbor/src/common/job/models"
|
||||
@ -27,12 +27,12 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/project"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/q"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -84,7 +84,7 @@ func NewLauncher(projectMgr project.Manager, repositoryMgr repository.Manager,
|
||||
|
||||
type jobData struct {
|
||||
TaskID int64
|
||||
Repository res.Repository
|
||||
Repository art.Repository
|
||||
JobName string
|
||||
JobParams map[string]interface{}
|
||||
}
|
||||
@ -111,9 +111,9 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
|
||||
if scope == nil {
|
||||
return 0, launcherError(fmt.Errorf("the scope of policy is nil"))
|
||||
}
|
||||
repositoryRules := make(map[res.Repository]*lwp.Metadata, 0)
|
||||
repositoryRules := make(map[art.Repository]*lwp.Metadata, 0)
|
||||
level := scope.Level
|
||||
var allProjects []*res.Candidate
|
||||
var allProjects []*art.Candidate
|
||||
var err error
|
||||
if level == "system" {
|
||||
// get projects
|
||||
@ -144,12 +144,12 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
|
||||
}
|
||||
}
|
||||
case "project":
|
||||
projectCandidates = append(projectCandidates, &res.Candidate{
|
||||
projectCandidates = append(projectCandidates, &art.Candidate{
|
||||
NamespaceID: scope.Reference,
|
||||
})
|
||||
}
|
||||
|
||||
var repositoryCandidates []*res.Candidate
|
||||
var repositoryCandidates []*art.Candidate
|
||||
// get repositories of projects
|
||||
for _, projectCandidate := range projectCandidates {
|
||||
repositories, err := getRepositories(l.projectMgr, l.repositoryMgr, projectCandidate.NamespaceID, l.chartServerEnabled)
|
||||
@ -174,7 +174,7 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
|
||||
}
|
||||
|
||||
for _, repositoryCandidate := range repositoryCandidates {
|
||||
reposit := res.Repository{
|
||||
reposit := art.Repository{
|
||||
Namespace: repositoryCandidate.Namespace,
|
||||
Name: repositoryCandidate.Repository,
|
||||
Kind: repositoryCandidate.Kind,
|
||||
@ -214,7 +214,7 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
|
||||
return int64(len(jobDatas)), nil
|
||||
}
|
||||
|
||||
func createJobs(repositoryRules map[res.Repository]*lwp.Metadata, isDryRun bool) ([]*jobData, error) {
|
||||
func createJobs(repositoryRules map[art.Repository]*lwp.Metadata, isDryRun bool) ([]*jobData, error) {
|
||||
jobDatas := []*jobData{}
|
||||
for repository, policy := range repositoryRules {
|
||||
jobData := &jobData{
|
||||
@ -320,14 +320,14 @@ func launcherError(err error) error {
|
||||
return errors.Wrap(err, "launcher")
|
||||
}
|
||||
|
||||
func getProjects(projectMgr project.Manager) ([]*res.Candidate, error) {
|
||||
func getProjects(projectMgr project.Manager) ([]*art.Candidate, error) {
|
||||
projects, err := projectMgr.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var candidates []*res.Candidate
|
||||
var candidates []*art.Candidate
|
||||
for _, pro := range projects {
|
||||
candidates = append(candidates, &res.Candidate{
|
||||
candidates = append(candidates, &art.Candidate{
|
||||
NamespaceID: pro.ProjectID,
|
||||
Namespace: pro.Name,
|
||||
})
|
||||
@ -336,8 +336,8 @@ func getProjects(projectMgr project.Manager) ([]*res.Candidate, error) {
|
||||
}
|
||||
|
||||
func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manager,
|
||||
projectID int64, chartServerEnabled bool) ([]*res.Candidate, error) {
|
||||
var candidates []*res.Candidate
|
||||
projectID int64, chartServerEnabled bool) ([]*art.Candidate, error) {
|
||||
var candidates []*art.Candidate
|
||||
/*
|
||||
pro, err := projectMgr.Get(projectID)
|
||||
if err != nil {
|
||||
@ -351,7 +351,7 @@ func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manage
|
||||
}
|
||||
for _, r := range imageRepositories {
|
||||
namespace, repo := utils.ParseRepository(r.Name)
|
||||
candidates = append(candidates, &res.Candidate{
|
||||
candidates = append(candidates, &art.Candidate{
|
||||
Namespace: namespace,
|
||||
Repository: repo,
|
||||
Kind: "image",
|
||||
@ -366,7 +366,7 @@ func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manage
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range chartRepositories {
|
||||
candidates = append(candidates, &res.Candidate{
|
||||
candidates = append(candidates, &art.Candidate{
|
||||
Namespace: pro.Name,
|
||||
Repository: r.Name,
|
||||
Kind: "chart",
|
||||
|
@ -21,12 +21,12 @@ import (
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common/job"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
_ "github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/project"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/q"
|
||||
_ "github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
|
||||
hjob "github.com/goharbor/harbor/src/testing/job"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -18,8 +18,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -29,7 +29,7 @@ import (
|
||||
type IndexTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
candidates []*res.Candidate
|
||||
candidates []*art.Candidate
|
||||
}
|
||||
|
||||
// TestIndexEntry is entry of IndexTestSuite
|
||||
@ -41,7 +41,7 @@ func TestIndexEntry(t *testing.T) {
|
||||
func (suite *IndexTestSuite) SetupSuite() {
|
||||
Register("fakeAction", newFakePerformer)
|
||||
|
||||
suite.candidates = []*res.Candidate{{
|
||||
suite.candidates = []*art.Candidate{{
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
Kind: "image",
|
||||
@ -77,9 +77,9 @@ type fakePerformer struct {
|
||||
}
|
||||
|
||||
// Perform the artifacts
|
||||
func (p *fakePerformer) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
|
||||
func (p *fakePerformer) Perform(candidates []*art.Candidate) (results []*art.Result, err error) {
|
||||
for _, c := range candidates {
|
||||
results = append(results, &res.Result{
|
||||
results = append(results, &art.Result{
|
||||
Target: c,
|
||||
})
|
||||
}
|
||||
|
@ -15,8 +15,8 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/dep"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -29,12 +29,12 @@ type Performer interface {
|
||||
// Perform the action
|
||||
//
|
||||
// Arguments:
|
||||
// candidates []*res.Candidate : the targets to perform
|
||||
// candidates []*art.Candidate : the targets to perform
|
||||
//
|
||||
// Returns:
|
||||
// []*res.Result : result infos
|
||||
// []*art.Result : result infos
|
||||
// error : common error if any errors occurred
|
||||
Perform(candidates []*res.Candidate) ([]*res.Result, error)
|
||||
Perform(candidates []*art.Candidate) ([]*art.Result, error)
|
||||
}
|
||||
|
||||
// PerformerFactory is factory method for creating Performer
|
||||
@ -42,13 +42,13 @@ type PerformerFactory func(params interface{}, isDryRun bool) Performer
|
||||
|
||||
// retainAction make sure all the candidates will be retained and others will be cleared
|
||||
type retainAction struct {
|
||||
all []*res.Candidate
|
||||
all []*art.Candidate
|
||||
// Indicate if it is a dry run
|
||||
isDryRun bool
|
||||
}
|
||||
|
||||
// Perform the action
|
||||
func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
|
||||
func (ra *retainAction) Perform(candidates []*art.Candidate) (results []*art.Result, err error) {
|
||||
retained := make(map[string]bool)
|
||||
for _, c := range candidates {
|
||||
retained[c.Hash()] = true
|
||||
@ -56,14 +56,14 @@ func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Res
|
||||
|
||||
// start to delete
|
||||
if len(ra.all) > 0 {
|
||||
for _, art := range ra.all {
|
||||
if _, ok := retained[art.Hash()]; !ok {
|
||||
result := &res.Result{
|
||||
Target: art,
|
||||
for _, c := range ra.all {
|
||||
if _, ok := retained[c.Hash()]; !ok {
|
||||
result := &art.Result{
|
||||
Target: c,
|
||||
}
|
||||
|
||||
if !ra.isDryRun {
|
||||
if err := dep.DefaultClient.Delete(art); err != nil {
|
||||
if err := dep.DefaultClient.Delete(c); err != nil {
|
||||
result.Error = err
|
||||
}
|
||||
}
|
||||
@ -79,7 +79,7 @@ func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Res
|
||||
// NewRetainAction is factory method for RetainAction
|
||||
func NewRetainAction(params interface{}, isDryRun bool) Performer {
|
||||
if params != nil {
|
||||
if all, ok := params.([]*res.Candidate); ok {
|
||||
if all, ok := params.([]*art.Candidate); ok {
|
||||
return &retainAction{
|
||||
all: all,
|
||||
isDryRun: isDryRun,
|
||||
@ -88,7 +88,7 @@ func NewRetainAction(params interface{}, isDryRun bool) Performer {
|
||||
}
|
||||
|
||||
return &retainAction{
|
||||
all: make([]*res.Candidate, 0),
|
||||
all: make([]*art.Candidate, 0),
|
||||
isDryRun: isDryRun,
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/dep"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -31,7 +31,7 @@ type TestPerformerSuite struct {
|
||||
suite.Suite
|
||||
|
||||
oldClient dep.Client
|
||||
all []*res.Candidate
|
||||
all []*art.Candidate
|
||||
}
|
||||
|
||||
// TestPerformer is the entry of the TestPerformerSuite
|
||||
@ -41,7 +41,7 @@ func TestPerformer(t *testing.T) {
|
||||
|
||||
// SetupSuite ...
|
||||
func (suite *TestPerformerSuite) SetupSuite() {
|
||||
suite.all = []*res.Candidate{
|
||||
suite.all = []*art.Candidate{
|
||||
{
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
@ -77,7 +77,7 @@ func (suite *TestPerformerSuite) TestPerform() {
|
||||
all: suite.all,
|
||||
}
|
||||
|
||||
candidates := []*res.Candidate{
|
||||
candidates := []*art.Candidate{
|
||||
{
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
@ -100,16 +100,16 @@ func (suite *TestPerformerSuite) TestPerform() {
|
||||
type fakeRetentionClient struct{}
|
||||
|
||||
// GetCandidates ...
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
|
||||
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRepository ...
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
@ -18,10 +18,10 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -29,7 +29,7 @@ import (
|
||||
type processor struct {
|
||||
// keep evaluator and its related selector if existing
|
||||
// attentions here, the selectors can be empty/nil, that means match all "**"
|
||||
evaluators map[*rule.Evaluator][]res.Selector
|
||||
evaluators map[*rule.Evaluator][]art.Selector
|
||||
// action performer
|
||||
performers map[string]action.Performer
|
||||
}
|
||||
@ -37,7 +37,7 @@ type processor struct {
|
||||
// New processor
|
||||
func New(parameters []*alg.Parameter) alg.Processor {
|
||||
p := &processor{
|
||||
evaluators: make(map[*rule.Evaluator][]res.Selector),
|
||||
evaluators: make(map[*rule.Evaluator][]art.Selector),
|
||||
performers: make(map[string]action.Performer),
|
||||
}
|
||||
|
||||
@ -59,10 +59,10 @@ func New(parameters []*alg.Parameter) alg.Processor {
|
||||
}
|
||||
|
||||
// Process the candidates with the rules
|
||||
func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
|
||||
func (p *processor) Process(artifacts []*art.Candidate) ([]*art.Result, error) {
|
||||
if len(artifacts) == 0 {
|
||||
log.Debug("no artifacts to retention")
|
||||
return make([]*res.Result, 0), nil
|
||||
return make([]*art.Result, 0), nil
|
||||
}
|
||||
|
||||
var (
|
||||
@ -75,7 +75,7 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
|
||||
// for sync
|
||||
type chanItem struct {
|
||||
action string
|
||||
processed []*res.Candidate
|
||||
processed []*art.Candidate
|
||||
}
|
||||
|
||||
resChan := make(chan *chanItem, 1)
|
||||
@ -124,9 +124,9 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
|
||||
for eva, selectors := range p.evaluators {
|
||||
var evaluator = *eva
|
||||
|
||||
go func(evaluator rule.Evaluator, selectors []res.Selector) {
|
||||
go func(evaluator rule.Evaluator, selectors []art.Selector) {
|
||||
var (
|
||||
processed []*res.Candidate
|
||||
processed []*art.Candidate
|
||||
err error
|
||||
)
|
||||
|
||||
@ -173,7 +173,7 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*res.Result, 0)
|
||||
results := make([]*art.Result, 0)
|
||||
// Perform actions
|
||||
for act, hash := range processedCandidates {
|
||||
var attachedErr error
|
||||
@ -192,7 +192,7 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
|
||||
|
||||
if attachedErr != nil {
|
||||
for _, c := range cl {
|
||||
results = append(results, &res.Result{
|
||||
results = append(results, &art.Result{
|
||||
Target: c,
|
||||
Error: attachedErr,
|
||||
})
|
||||
@ -203,10 +203,10 @@ func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
type cHash map[string]*res.Candidate
|
||||
type cHash map[string]*art.Candidate
|
||||
|
||||
func (ch cHash) toList() []*res.Candidate {
|
||||
l := make([]*res.Candidate, 0)
|
||||
func (ch cHash) toList() []*art.Candidate {
|
||||
l := make([]*art.Candidate, 0)
|
||||
|
||||
for _, v := range ch {
|
||||
l = append(l, v)
|
||||
|
@ -19,6 +19,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/label"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/dep"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
|
||||
@ -26,9 +29,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/always"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/lastx"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestps"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -38,7 +38,7 @@ import (
|
||||
type ProcessorTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
all []*res.Candidate
|
||||
all []*art.Candidate
|
||||
|
||||
oldClient dep.Client
|
||||
}
|
||||
@ -50,7 +50,7 @@ func TestProcessor(t *testing.T) {
|
||||
|
||||
// SetupSuite ...
|
||||
func (suite *ProcessorTestSuite) SetupSuite() {
|
||||
suite.all = []*res.Candidate{
|
||||
suite.all = []*art.Candidate{
|
||||
{
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
@ -90,7 +90,7 @@ func (suite *ProcessorTestSuite) TestProcess() {
|
||||
lastxParams[lastx.ParameterX] = 10
|
||||
params = append(params, &alg.Parameter{
|
||||
Evaluator: lastx.New(lastxParams),
|
||||
Selectors: []res.Selector{
|
||||
Selectors: []art.Selector{
|
||||
doublestar.New(doublestar.Matches, "*dev*"),
|
||||
label.New(label.With, "L1,L2"),
|
||||
},
|
||||
@ -101,7 +101,7 @@ func (suite *ProcessorTestSuite) TestProcess() {
|
||||
latestKParams[latestps.ParameterK] = 10
|
||||
params = append(params, &alg.Parameter{
|
||||
Evaluator: latestps.New(latestKParams),
|
||||
Selectors: []res.Selector{
|
||||
Selectors: []art.Selector{
|
||||
label.New(label.With, "L3"),
|
||||
},
|
||||
Performer: perf,
|
||||
@ -131,7 +131,7 @@ func (suite *ProcessorTestSuite) TestProcess2() {
|
||||
alwaysParams := make(map[string]rule.Parameter)
|
||||
params = append(params, &alg.Parameter{
|
||||
Evaluator: always.New(alwaysParams),
|
||||
Selectors: []res.Selector{
|
||||
Selectors: []art.Selector{
|
||||
doublestar.New(doublestar.Matches, "latest"),
|
||||
label.New(label.With, ""),
|
||||
},
|
||||
@ -163,16 +163,16 @@ func (suite *ProcessorTestSuite) TestProcess2() {
|
||||
type fakeRetentionClient struct{}
|
||||
|
||||
// GetCandidates ...
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
|
||||
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRepository ...
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
package alg
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
// Processor processing the whole policy targeting a repository.
|
||||
@ -27,12 +27,12 @@ type Processor interface {
|
||||
// Process the artifact candidates
|
||||
//
|
||||
// Arguments:
|
||||
// artifacts []*res.Candidate : process the retention candidates
|
||||
// artifacts []*art.Candidate : process the retention candidates
|
||||
//
|
||||
// Returns:
|
||||
// []*res.Result : the processed results
|
||||
// []*art.Result : the processed results
|
||||
// error : common error object if any errors occurred
|
||||
Process(artifacts []*res.Candidate) ([]*res.Result, error)
|
||||
Process(artifacts []*art.Candidate) ([]*art.Result, error)
|
||||
}
|
||||
|
||||
// Parameter for constructing a processor
|
||||
@ -42,7 +42,7 @@ type Parameter struct {
|
||||
Evaluator rule.Evaluator
|
||||
|
||||
// Selectors for the rule
|
||||
Selectors []res.Selector
|
||||
Selectors []art.Selector
|
||||
|
||||
// Performer for the rule evaluator
|
||||
Performer action.Performer
|
||||
|
@ -21,13 +21,13 @@ import (
|
||||
|
||||
index3 "github.com/goharbor/harbor/src/pkg/retention/policy/alg/index"
|
||||
|
||||
index2 "github.com/goharbor/harbor/src/pkg/retention/res/selectors/index"
|
||||
index2 "github.com/goharbor/harbor/src/pkg/art/selectors/index"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/index"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -46,7 +46,7 @@ type Builder interface {
|
||||
}
|
||||
|
||||
// NewBuilder news a basic builder
|
||||
func NewBuilder(all []*res.Candidate) Builder {
|
||||
func NewBuilder(all []*art.Candidate) Builder {
|
||||
return &basicBuilder{
|
||||
allCandidates: all,
|
||||
}
|
||||
@ -54,7 +54,7 @@ func NewBuilder(all []*res.Candidate) Builder {
|
||||
|
||||
// basicBuilder is default implementation of Builder interface
|
||||
type basicBuilder struct {
|
||||
allCandidates []*res.Candidate
|
||||
allCandidates []*art.Candidate
|
||||
}
|
||||
|
||||
// Build policy processor from the raw policy
|
||||
@ -76,7 +76,7 @@ func (bb *basicBuilder) Build(policy *lwp.Metadata, isDryRun bool) (alg.Processo
|
||||
return nil, errors.Wrap(err, "get action performer by metadata")
|
||||
}
|
||||
|
||||
sl := make([]res.Selector, 0)
|
||||
sl := make([]art.Selector, 0)
|
||||
for _, s := range r.TagSelectors {
|
||||
sel, err := index2.Get(s.Kind, s.Decoration, s.Pattern)
|
||||
if err != nil {
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
|
||||
index2 "github.com/goharbor/harbor/src/pkg/retention/policy/alg/index"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/index"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/index"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/dep"
|
||||
|
||||
@ -30,9 +30,9 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/alg/or"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/label"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
|
||||
"github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestps"
|
||||
|
||||
@ -46,7 +46,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -55,7 +55,7 @@ import (
|
||||
type TestBuilderSuite struct {
|
||||
suite.Suite
|
||||
|
||||
all []*res.Candidate
|
||||
all []*art.Candidate
|
||||
oldClient dep.Client
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ func TestBuilder(t *testing.T) {
|
||||
|
||||
// SetupSuite prepares the testing content if needed
|
||||
func (suite *TestBuilderSuite) SetupSuite() {
|
||||
suite.all = []*res.Candidate{
|
||||
suite.all = []*art.Candidate{
|
||||
{
|
||||
NamespaceID: 1,
|
||||
Namespace: "library",
|
||||
@ -163,21 +163,21 @@ func (suite *TestBuilderSuite) TestBuild() {
|
||||
|
||||
type fakeRetentionClient struct{}
|
||||
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *res.Repository) error {
|
||||
func (frc *fakeRetentionClient) DeleteRepository(repo *art.Repository) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
// GetCandidates ...
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
|
||||
func (frc *fakeRetentionClient) GetCandidates(repo *art.Repository) ([]*art.Candidate, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
|
||||
func (frc *fakeRetentionClient) Delete(candidate *art.Candidate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitTask ...
|
||||
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
|
||||
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *art.Repository, meta *lwp.Metadata) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
package always
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -28,7 +28,7 @@ const (
|
||||
type evaluator struct{}
|
||||
|
||||
// Process for the "always" Evaluator simply returns the input with no error
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
|
||||
return artifacts, nil
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,8 @@ package always
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -36,7 +36,7 @@ func (e *EvaluatorTestSuite) TestNew() {
|
||||
|
||||
func (e *EvaluatorTestSuite) TestProcess() {
|
||||
sut := New(rule.Parameters{})
|
||||
input := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
|
||||
input := []*art.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
|
||||
|
||||
result, err := sut.Process(input)
|
||||
|
||||
|
@ -20,9 +20,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -41,7 +41,7 @@ type evaluator struct {
|
||||
n int
|
||||
}
|
||||
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) (result []*res.Candidate, err error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) (result []*art.Candidate, err error) {
|
||||
minPullTime := time.Now().UTC().Add(time.Duration(-1*24*e.n) * time.Hour).Unix()
|
||||
for _, a := range artifacts {
|
||||
if a.PulledTime >= minPullTime {
|
||||
|
@ -20,8 +20,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -54,7 +54,7 @@ func (e *EvaluatorTestSuite) TestNew() {
|
||||
|
||||
func (e *EvaluatorTestSuite) TestProcess() {
|
||||
now := time.Now().UTC()
|
||||
data := []*res.Candidate{
|
||||
data := []*art.Candidate{
|
||||
{PulledTime: daysAgo(now, 1)},
|
||||
{PulledTime: daysAgo(now, 2)},
|
||||
{PulledTime: daysAgo(now, 3)},
|
||||
|
@ -20,9 +20,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -41,7 +41,7 @@ type evaluator struct {
|
||||
n int
|
||||
}
|
||||
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) (result []*res.Candidate, err error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) (result []*art.Candidate, err error) {
|
||||
minPushTime := time.Now().UTC().Add(time.Duration(-1*24*e.n) * time.Hour).Unix()
|
||||
for _, a := range artifacts {
|
||||
if a.PushedTime >= minPushTime {
|
||||
|
@ -20,8 +20,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -54,7 +54,7 @@ func (e *EvaluatorTestSuite) TestNew() {
|
||||
|
||||
func (e *EvaluatorTestSuite) TestProcess() {
|
||||
now := time.Now().UTC()
|
||||
data := []*res.Candidate{
|
||||
data := []*art.Candidate{
|
||||
{PushedTime: daysAgo(now, 1)},
|
||||
{PushedTime: daysAgo(now, 2)},
|
||||
{PushedTime: daysAgo(now, 3)},
|
||||
|
@ -14,19 +14,19 @@
|
||||
|
||||
package rule
|
||||
|
||||
import "github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
import "github.com/goharbor/harbor/src/pkg/art"
|
||||
|
||||
// Evaluator defines method of executing rule
|
||||
type Evaluator interface {
|
||||
// Filter the inputs and return the filtered outputs
|
||||
//
|
||||
// Arguments:
|
||||
// artifacts []*res.Candidate : candidates for processing
|
||||
// artifacts []*art.Candidate : candidates for processing
|
||||
//
|
||||
// Returns:
|
||||
// []*res.Candidate : matched candidates for next stage
|
||||
// []*art.Candidate : matched candidates for next stage
|
||||
// error : common error object if any errors occurred
|
||||
Process(artifacts []*res.Candidate) ([]*res.Candidate, error)
|
||||
Process(artifacts []*art.Candidate) ([]*art.Candidate, error)
|
||||
|
||||
// Specify what action is performed to the candidates processed by this evaluator
|
||||
Action() string
|
||||
|
@ -22,8 +22,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -63,7 +63,7 @@ func (suite *IndexTestSuite) TestGet() {
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), evaluator)
|
||||
|
||||
candidates := []*res.Candidate{{
|
||||
candidates := []*art.Candidate{{
|
||||
Namespace: "library",
|
||||
Repository: "harbor",
|
||||
Kind: "image",
|
||||
@ -102,7 +102,7 @@ type fakeEvaluator struct {
|
||||
}
|
||||
|
||||
// Process rule
|
||||
func (e *fakeEvaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
|
||||
func (e *fakeEvaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
|
||||
return artifacts, nil
|
||||
}
|
||||
|
||||
|
@ -19,9 +19,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -40,7 +40,7 @@ type evaluator struct {
|
||||
}
|
||||
|
||||
// Process the candidates based on the rule definition
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) (retain []*res.Candidate, err error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) (retain []*art.Candidate, err error) {
|
||||
cutoff := time.Now().Add(time.Duration(e.x*-24) * time.Hour)
|
||||
for _, a := range artifacts {
|
||||
if time.Unix(a.PushedTime, 0).UTC().After(cutoff) {
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -38,7 +38,7 @@ func (e *EvaluatorTestSuite) TestNew() {
|
||||
|
||||
func (e *EvaluatorTestSuite) TestProcess() {
|
||||
now := time.Now().UTC()
|
||||
data := []*res.Candidate{
|
||||
data := []*art.Candidate{
|
||||
{PushedTime: now.Add(time.Duration(1*-24) * time.Hour).Unix()},
|
||||
{PushedTime: now.Add(time.Duration(2*-24) * time.Hour).Unix()},
|
||||
{PushedTime: now.Add(time.Duration(3*-24) * time.Hour).Unix()},
|
||||
|
@ -19,9 +19,9 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -40,7 +40,7 @@ type evaluator struct {
|
||||
}
|
||||
|
||||
// Process the candidates based on the rule definition
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
|
||||
// Sort artifacts by their "active time"
|
||||
//
|
||||
// Active time is defined as the selection of c.PulledTime or c.PushedTime,
|
||||
@ -81,7 +81,7 @@ func New(params rule.Parameters) rule.Evaluator {
|
||||
}
|
||||
}
|
||||
|
||||
func activeTime(c *res.Candidate) int64 {
|
||||
func activeTime(c *art.Candidate) int64 {
|
||||
if c.PulledTime > c.PushedTime {
|
||||
return c.PulledTime
|
||||
}
|
||||
|
@ -22,18 +22,18 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type EvaluatorTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
artifacts []*res.Candidate
|
||||
artifacts []*art.Candidate
|
||||
}
|
||||
|
||||
func (e *EvaluatorTestSuite) SetupSuite() {
|
||||
e.artifacts = []*res.Candidate{
|
||||
e.artifacts = []*art.Candidate{
|
||||
{PulledTime: 1, PushedTime: 2},
|
||||
{PulledTime: 3, PushedTime: 4},
|
||||
{PulledTime: 6, PushedTime: 5},
|
||||
|
@ -21,9 +21,9 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -41,7 +41,7 @@ type evaluator struct {
|
||||
n int
|
||||
}
|
||||
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
|
||||
sort.Slice(artifacts, func(i, j int) bool {
|
||||
return artifacts[i].PulledTime > artifacts[j].PulledTime
|
||||
})
|
||||
|
@ -20,8 +20,8 @@ import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -52,7 +52,7 @@ func (e *EvaluatorTestSuite) TestNew() {
|
||||
}
|
||||
|
||||
func (e *EvaluatorTestSuite) TestProcess() {
|
||||
data := []*res.Candidate{{PulledTime: 0}, {PulledTime: 1}, {PulledTime: 2}, {PulledTime: 3}, {PulledTime: 4}}
|
||||
data := []*art.Candidate{{PulledTime: 0}, {PulledTime: 1}, {PulledTime: 2}, {PulledTime: 3}, {PulledTime: 4}}
|
||||
rand.Shuffle(len(data), func(i, j int) {
|
||||
data[i], data[j] = data[j], data[i]
|
||||
})
|
||||
|
@ -21,9 +21,9 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -42,7 +42,7 @@ type evaluator struct {
|
||||
}
|
||||
|
||||
// Process the candidates based on the rule definition
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) ([]*art.Candidate, error) {
|
||||
// The updated proposal does not guarantee the order artifacts are provided, so we have to sort them first
|
||||
sort.Slice(artifacts, func(i, j int) bool {
|
||||
return artifacts[i].PushedTime > artifacts[j].PushedTime
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -39,7 +39,7 @@ func (e *EvaluatorTestSuite) TestNew() {
|
||||
}
|
||||
|
||||
func (e *EvaluatorTestSuite) TestProcess() {
|
||||
data := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}, {PushedTime: 4}}
|
||||
data := []*art.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}, {PushedTime: 4}}
|
||||
rand.Shuffle(len(data), func(i, j int) {
|
||||
data[i], data[j] = data[j], data[i]
|
||||
})
|
||||
|
@ -15,9 +15,9 @@
|
||||
package nothing
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -28,7 +28,7 @@ const (
|
||||
type evaluator struct{}
|
||||
|
||||
// Process for the "nothing" Evaluator simply returns the input with no error
|
||||
func (e *evaluator) Process(artifacts []*res.Candidate) (processed []*res.Candidate, err error) {
|
||||
func (e *evaluator) Process(artifacts []*art.Candidate) (processed []*art.Candidate, err error) {
|
||||
return processed, err
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,8 @@ package nothing
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/res"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -36,7 +36,7 @@ func (e *EvaluatorTestSuite) TestNew() {
|
||||
|
||||
func (e *EvaluatorTestSuite) TestProcess() {
|
||||
sut := New(rule.Parameters{})
|
||||
input := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
|
||||
input := []*art.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
|
||||
|
||||
result, err := sut.Process(input)
|
||||
|
||||
|
157
src/pkg/scan/scanner/api/controller.go
Normal file
157
src/pkg/scan/scanner/api/controller.go
Normal file
@ -0,0 +1,157 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
|
||||
)
|
||||
|
||||
// Controller provides the related operations of scanner for the upper API.
|
||||
// All the capabilities of the scanner are defined here.
|
||||
type Controller interface {
|
||||
// ListRegistrations returns a list of currently configured scanner registrations.
|
||||
// Query parameters are optional
|
||||
//
|
||||
// Arguments:
|
||||
// query *q.Query : query parameters
|
||||
//
|
||||
// Returns:
|
||||
// []*scanner.Registration : scanner list of all the matched ones
|
||||
// error : non nil error if any errors occurred
|
||||
ListRegistrations(query *q.Query) ([]*scanner.Registration, error)
|
||||
|
||||
// CreateRegistration creates a new scanner registration with the given data.
|
||||
// Returns the scanner registration identifier.
|
||||
//
|
||||
// Arguments:
|
||||
// registration *scanner.Registration : scanner registration to create
|
||||
//
|
||||
// Returns:
|
||||
// string : the generated UUID of the new scanner
|
||||
// error : non nil error if any errors occurred
|
||||
CreateRegistration(registration *scanner.Registration) (string, error)
|
||||
|
||||
// GetRegistration returns the details of the specified scanner registration.
|
||||
//
|
||||
// Arguments:
|
||||
// registrationUUID string : the UUID of the given scanner
|
||||
//
|
||||
// Returns:
|
||||
// *scanner.Registration : the required scanner
|
||||
// error : non nil error if any errors occurred
|
||||
GetRegistration(registrationUUID string) (*scanner.Registration, error)
|
||||
|
||||
// RegistrationExists checks if the provided registration is there.
|
||||
//
|
||||
// Arguments:
|
||||
// registrationUUID string : the UUID of the given scanner
|
||||
//
|
||||
// Returns:
|
||||
// true for existing or false for not existing
|
||||
RegistrationExists(registrationUUID string) bool
|
||||
|
||||
// UpdateRegistration updates the specified scanner registration.
|
||||
//
|
||||
// Arguments:
|
||||
// registration *scanner.Registration : scanner registration to update
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
UpdateRegistration(registration *scanner.Registration) error
|
||||
|
||||
// DeleteRegistration deletes the specified scanner registration.
|
||||
//
|
||||
// Arguments:
|
||||
// registrationUUID string : the UUID of the given scanner which is going to be deleted
|
||||
//
|
||||
// Returns:
|
||||
// *scanner.Registration : the deleted scanner
|
||||
// error : non nil error if any errors occurred
|
||||
DeleteRegistration(registrationUUID string) (*scanner.Registration, error)
|
||||
|
||||
// SetDefaultRegistration marks the specified scanner registration as default.
|
||||
// The implementation is supposed to unset any registration previously set as default.
|
||||
//
|
||||
// Arguments:
|
||||
// registrationUUID string : the UUID of the given scanner which is marked as default
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
SetDefaultRegistration(registrationUUID string) error
|
||||
|
||||
// SetRegistrationByProject sets scanner for the given project.
|
||||
//
|
||||
// Arguments:
|
||||
// projectID int64 : the ID of the given project
|
||||
// scannerID string : the UUID of the the scanner
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
SetRegistrationByProject(projectID int64, scannerID string) error
|
||||
|
||||
// GetRegistrationByProject returns the configured scanner registration of the given project or
|
||||
// the system default registration if exists or `nil` if no system registrations set.
|
||||
//
|
||||
// Arguments:
|
||||
// projectID int64 : the ID of the given project
|
||||
//
|
||||
// Returns:
|
||||
// *scanner.Registration : the default scanner registration
|
||||
// error : non nil error if any errors occurred
|
||||
GetRegistrationByProject(projectID int64) (*scanner.Registration, error)
|
||||
|
||||
// Ping pings Scanner Adapter to test EndpointURL and Authorization settings.
|
||||
// The implementation is supposed to call the GetMetadata method on scanner.Client.
|
||||
// Returns `nil` if connection succeeded, a non `nil` error otherwise.
|
||||
//
|
||||
// Arguments:
|
||||
// registration *scanner.Registration : scanner registration to ping
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
Ping(registration *scanner.Registration) error
|
||||
|
||||
// Scan the given artifact
|
||||
//
|
||||
// Arguments:
|
||||
// artifact *res.Artifact : artifact to be scanned
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
Scan(artifact *scan.Artifact) error
|
||||
|
||||
// GetReport gets the reports for the given artifact identified by the digest
|
||||
//
|
||||
// Arguments:
|
||||
// artifact *res.Artifact : the scanned artifact
|
||||
//
|
||||
// Returns:
|
||||
// []*scan.Report : scan results by different scanner vendors
|
||||
// error : non nil error if any errors occurred
|
||||
GetReport(artifact *scan.Artifact) ([]*dscan.Report, error)
|
||||
|
||||
// Get the scan log for the specified artifact with the given digest
|
||||
//
|
||||
// Arguments:
|
||||
// digest string : the digest of the artifact
|
||||
//
|
||||
// Returns:
|
||||
// []byte : the log text stream
|
||||
// error : non nil error if any errors occurred
|
||||
GetScanLog(digest string) ([]byte, error)
|
||||
}
|
287
src/pkg/scan/scanner/api/controller_test.go
Normal file
287
src/pkg/scan/scanner/api/controller_test.go
Normal file
@ -0,0 +1,287 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// ControllerTestSuite is test suite to test the basic api controller.
|
||||
type ControllerTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
c *basicController
|
||||
mMgr *MockScannerManager
|
||||
mMeta *MockProMetaManager
|
||||
|
||||
sample *scanner.Registration
|
||||
}
|
||||
|
||||
// TestController is the entry of controller test suite
|
||||
func TestController(t *testing.T) {
|
||||
suite.Run(t, new(ControllerTestSuite))
|
||||
}
|
||||
|
||||
// SetupSuite prepares env for the controller test suite
|
||||
func (suite *ControllerTestSuite) SetupSuite() {
|
||||
suite.mMgr = new(MockScannerManager)
|
||||
suite.mMeta = new(MockProMetaManager)
|
||||
|
||||
suite.c = &basicController{
|
||||
manager: suite.mMgr,
|
||||
proMetaMgr: suite.mMeta,
|
||||
}
|
||||
|
||||
suite.sample = &scanner.Registration{
|
||||
Name: "forUT",
|
||||
Description: "sample registration",
|
||||
URL: "https://sample.scanner.com",
|
||||
Adapter: "Clair",
|
||||
Version: "0.1.0",
|
||||
Vendor: "Harbor",
|
||||
}
|
||||
}
|
||||
|
||||
// Clear test case
|
||||
func (suite *ControllerTestSuite) TearDownTest() {
|
||||
suite.sample.UUID = ""
|
||||
}
|
||||
|
||||
// TestListRegistrations tests ListRegistrations
|
||||
func (suite *ControllerTestSuite) TestListRegistrations() {
|
||||
query := &q.Query{
|
||||
PageSize: 10,
|
||||
PageNumber: 1,
|
||||
}
|
||||
|
||||
suite.sample.UUID = "uuid"
|
||||
l := []*scanner.Registration{suite.sample}
|
||||
|
||||
suite.mMgr.On("List", query).Return(l, nil)
|
||||
|
||||
rl, err := suite.c.ListRegistrations(query)
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), 1, len(rl))
|
||||
}
|
||||
|
||||
// TestCreateRegistration tests CreateRegistration
|
||||
func (suite *ControllerTestSuite) TestCreateRegistration() {
|
||||
suite.mMgr.On("Create", suite.sample).Return("uuid", nil)
|
||||
|
||||
uid, err := suite.mMgr.Create(suite.sample)
|
||||
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), uid, "uuid")
|
||||
}
|
||||
|
||||
// TestGetRegistration tests GetRegistration
|
||||
func (suite *ControllerTestSuite) TestGetRegistration() {
|
||||
suite.sample.UUID = "uuid"
|
||||
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
|
||||
|
||||
rr, err := suite.c.GetRegistration("uuid")
|
||||
require.NoError(suite.T(), err)
|
||||
assert.NotNil(suite.T(), rr)
|
||||
assert.Equal(suite.T(), "forUT", rr.Name)
|
||||
}
|
||||
|
||||
// TestRegistrationExists tests RegistrationExists
|
||||
func (suite *ControllerTestSuite) TestRegistrationExists() {
|
||||
suite.sample.UUID = "uuid"
|
||||
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
|
||||
|
||||
exists := suite.c.RegistrationExists("uuid")
|
||||
assert.Equal(suite.T(), true, exists)
|
||||
|
||||
suite.mMgr.On("Get", "uuid2").Return(nil, nil)
|
||||
|
||||
exists = suite.c.RegistrationExists("uuid2")
|
||||
assert.Equal(suite.T(), false, exists)
|
||||
}
|
||||
|
||||
// TestUpdateRegistration tests UpdateRegistration
|
||||
func (suite *ControllerTestSuite) TestUpdateRegistration() {
|
||||
suite.sample.UUID = "uuid"
|
||||
suite.mMgr.On("Update", suite.sample).Return(nil)
|
||||
|
||||
err := suite.c.UpdateRegistration(suite.sample)
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
||||
|
||||
// TestDeleteRegistration tests DeleteRegistration
|
||||
func (suite *ControllerTestSuite) TestDeleteRegistration() {
|
||||
suite.sample.UUID = "uuid"
|
||||
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
|
||||
suite.mMgr.On("Delete", "uuid").Return(nil)
|
||||
|
||||
r, err := suite.c.DeleteRegistration("uuid")
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
assert.Equal(suite.T(), "forUT", r.Name)
|
||||
}
|
||||
|
||||
// TestSetDefaultRegistration tests SetDefaultRegistration
|
||||
func (suite *ControllerTestSuite) TestSetDefaultRegistration() {
|
||||
suite.mMgr.On("SetAsDefault", "uuid").Return(nil)
|
||||
|
||||
err := suite.c.SetDefaultRegistration("uuid")
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
||||
|
||||
// TestSetRegistrationByProject tests SetRegistrationByProject
|
||||
func (suite *ControllerTestSuite) TestSetRegistrationByProject() {
|
||||
m := make(map[string]string, 1)
|
||||
mm := make(map[string]string, 1)
|
||||
mmm := make(map[string]string, 1)
|
||||
mm[proScannerMetaKey] = "uuid"
|
||||
mmm[proScannerMetaKey] = "uuid2"
|
||||
|
||||
var pid, pid2 int64 = 1, 2
|
||||
|
||||
// not set before
|
||||
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil)
|
||||
suite.mMeta.On("Add", pid, mm).Return(nil)
|
||||
|
||||
err := suite.c.SetRegistrationByProject(pid, "uuid")
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
// Set before
|
||||
suite.mMeta.On("Get", pid2, []string{proScannerMetaKey}).Return(mm, nil)
|
||||
suite.mMeta.On("Update", pid2, mmm).Return(nil)
|
||||
|
||||
err = suite.c.SetRegistrationByProject(pid2, "uuid2")
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
||||
|
||||
// TestGetRegistrationByProject tests GetRegistrationByProject
|
||||
func (suite *ControllerTestSuite) TestGetRegistrationByProject() {
|
||||
m := make(map[string]string, 1)
|
||||
m[proScannerMetaKey] = "uuid"
|
||||
|
||||
// Configured at project level
|
||||
var pid int64 = 1
|
||||
suite.sample.UUID = "uuid"
|
||||
|
||||
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil)
|
||||
suite.mMgr.On("Get", "uuid").Return(suite.sample, nil)
|
||||
|
||||
r, err := suite.c.GetRegistrationByProject(pid)
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), "forUT", r.Name)
|
||||
|
||||
// Not configured at project level, return system default
|
||||
suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(nil, nil)
|
||||
suite.mMgr.On("GetDefault").Return(suite.sample, nil)
|
||||
|
||||
r, err = suite.c.GetRegistrationByProject(pid)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
assert.Equal(suite.T(), "forUT", r.Name)
|
||||
}
|
||||
|
||||
// MockScannerManager is mock of the scanner manager
|
||||
type MockScannerManager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// List ...
|
||||
func (m *MockScannerManager) List(query *q.Query) ([]*scanner.Registration, error) {
|
||||
args := m.Called(query)
|
||||
return args.Get(0).([]*scanner.Registration), args.Error(1)
|
||||
}
|
||||
|
||||
// Create ...
|
||||
func (m *MockScannerManager) Create(registration *scanner.Registration) (string, error) {
|
||||
args := m.Called(registration)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (m *MockScannerManager) Get(registrationUUID string) (*scanner.Registration, error) {
|
||||
args := m.Called(registrationUUID)
|
||||
r := args.Get(0)
|
||||
if r == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return r.(*scanner.Registration), args.Error(1)
|
||||
}
|
||||
|
||||
// Update ...
|
||||
func (m *MockScannerManager) Update(registration *scanner.Registration) error {
|
||||
args := m.Called(registration)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (m *MockScannerManager) Delete(registrationUUID string) error {
|
||||
args := m.Called(registrationUUID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// SetAsDefault ...
|
||||
func (m *MockScannerManager) SetAsDefault(registrationUUID string) error {
|
||||
args := m.Called(registrationUUID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// GetDefault ...
|
||||
func (m *MockScannerManager) GetDefault() (*scanner.Registration, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(*scanner.Registration), args.Error(1)
|
||||
}
|
||||
|
||||
// MockProMetaManager is the mock of the ProjectMetadataManager
|
||||
type MockProMetaManager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Add ...
|
||||
func (m *MockProMetaManager) Add(projectID int64, meta map[string]string) error {
|
||||
args := m.Called(projectID, meta)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (m *MockProMetaManager) Delete(projecdtID int64, meta ...string) error {
|
||||
args := m.Called(projecdtID, meta)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Update ...
|
||||
func (m *MockProMetaManager) Update(projectID int64, meta map[string]string) error {
|
||||
args := m.Called(projectID, meta)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (m *MockProMetaManager) Get(projectID int64, meta ...string) (map[string]string, error) {
|
||||
args := m.Called(projectID, meta)
|
||||
return args.Get(0).(map[string]string), args.Error(1)
|
||||
}
|
||||
|
||||
// List ...
|
||||
func (m *MockProMetaManager) List(name, value string) ([]*models.ProjectMetadata, error) {
|
||||
args := m.Called(name, value)
|
||||
return args.Get(0).([]*models.ProjectMetadata), args.Error(1)
|
||||
}
|
194
src/pkg/scan/scanner/api/registration.go
Normal file
194
src/pkg/scan/scanner/api/registration.go
Normal file
@ -0,0 +1,194 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/core/promgr/metamgr"
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
rscanner "github.com/goharbor/harbor/src/pkg/scan/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
proScannerMetaKey = "projectScanner"
|
||||
)
|
||||
|
||||
// DefaultController is a singleton api controller for plug scanners
|
||||
var DefaultController = New()
|
||||
|
||||
// New a basic controller
|
||||
func New() Controller {
|
||||
return &basicController{
|
||||
manager: rscanner.New(),
|
||||
proMetaMgr: metamgr.NewDefaultProjectMetadataManager(),
|
||||
}
|
||||
}
|
||||
|
||||
// basicController is default implementation of api.Controller interface
|
||||
type basicController struct {
|
||||
// managers for managing the scanner registrations
|
||||
manager rscanner.Manager
|
||||
// for operating the project level configured scanner
|
||||
proMetaMgr metamgr.ProjectMetadataManager
|
||||
// controller for scan actions
|
||||
c scan.Controller
|
||||
// Client
|
||||
}
|
||||
|
||||
// ListRegistrations ...
|
||||
func (bc *basicController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) {
|
||||
return bc.manager.List(query)
|
||||
}
|
||||
|
||||
// CreateRegistration ...
|
||||
func (bc *basicController) CreateRegistration(registration *scanner.Registration) (string, error) {
|
||||
// TODO: Get metadata from the adapter service first
|
||||
l, err := bc.manager.List(nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(l) == 0 && !registration.IsDefault {
|
||||
// Mark the 1st as default automatically
|
||||
registration.IsDefault = true
|
||||
}
|
||||
|
||||
return bc.manager.Create(registration)
|
||||
}
|
||||
|
||||
// GetRegistration ...
|
||||
func (bc *basicController) GetRegistration(registrationUUID string) (*scanner.Registration, error) {
|
||||
return bc.manager.Get(registrationUUID)
|
||||
}
|
||||
|
||||
// RegistrationExists ...
|
||||
func (bc *basicController) RegistrationExists(registrationUUID string) bool {
|
||||
registration, err := bc.manager.Get(registrationUUID)
|
||||
|
||||
// Just logged when an error occurred
|
||||
if err != nil {
|
||||
logger.Errorf("Check existence of registration error: %s", err)
|
||||
}
|
||||
|
||||
return !(err == nil && registration == nil)
|
||||
}
|
||||
|
||||
// UpdateRegistration ...
|
||||
func (bc *basicController) UpdateRegistration(registration *scanner.Registration) error {
|
||||
return bc.manager.Update(registration)
|
||||
}
|
||||
|
||||
// SetDefaultRegistration ...
|
||||
func (bc *basicController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) {
|
||||
registration, err := bc.manager.Get(registrationUUID)
|
||||
if registration == nil && err == nil {
|
||||
// Not found
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := bc.manager.Delete(registrationUUID); err != nil {
|
||||
return nil, errors.Wrap(err, "delete registration")
|
||||
}
|
||||
|
||||
return registration, nil
|
||||
}
|
||||
|
||||
// SetDefaultRegistration ...
|
||||
func (bc *basicController) SetDefaultRegistration(registrationUUID string) error {
|
||||
return bc.manager.SetAsDefault(registrationUUID)
|
||||
}
|
||||
|
||||
// SetRegistrationByProject ...
|
||||
func (bc *basicController) SetRegistrationByProject(projectID int64, registrationID string) error {
|
||||
if projectID == 0 {
|
||||
return errors.New("invalid project ID")
|
||||
}
|
||||
|
||||
if len(registrationID) == 0 {
|
||||
return errors.New("missing scanner UUID")
|
||||
}
|
||||
|
||||
// Only keep the UUID in the metadata of the given project
|
||||
// Scanner metadata existing?
|
||||
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "set project scanner")
|
||||
}
|
||||
|
||||
// Update if exists
|
||||
if len(m) > 0 {
|
||||
// Compare and set new
|
||||
if registrationID != m[proScannerMetaKey] {
|
||||
m[proScannerMetaKey] = registrationID
|
||||
if err := bc.proMetaMgr.Update(projectID, m); err != nil {
|
||||
return errors.Wrap(err, "set project scanner")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
meta := make(map[string]string, 1)
|
||||
meta[proScannerMetaKey] = registrationID
|
||||
if err := bc.proMetaMgr.Add(projectID, meta); err != nil {
|
||||
return errors.Wrap(err, "set project scanner")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRegistrationByProject ...
|
||||
func (bc *basicController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) {
|
||||
if projectID == 0 {
|
||||
return nil, errors.New("invalid project ID")
|
||||
}
|
||||
|
||||
// First, get it from the project metadata
|
||||
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get project scanner")
|
||||
}
|
||||
|
||||
if len(m) > 0 {
|
||||
if registrationID, ok := m[proScannerMetaKey]; ok && len(registrationID) > 0 {
|
||||
registration, err := bc.manager.Get(registrationID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get project scanner")
|
||||
}
|
||||
|
||||
if registration == nil {
|
||||
// Not found
|
||||
// Might be deleted by the admin, the project scanner ID reference should be cleared
|
||||
if err := bc.proMetaMgr.Delete(projectID, proScannerMetaKey); err != nil {
|
||||
return nil, errors.Wrap(err, "get project scanner")
|
||||
}
|
||||
} else {
|
||||
return registration, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second, get the default one
|
||||
registration, err := bc.manager.GetDefault()
|
||||
|
||||
// TODO: Check status by the client later
|
||||
return registration, err
|
||||
}
|
||||
|
||||
// Ping ...
|
||||
func (bc *basicController) Ping(registration *scanner.Registration) error {
|
||||
return nil
|
||||
}
|
35
src/pkg/scan/scanner/api/scan.go
Normal file
35
src/pkg/scan/scanner/api/scan.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
|
||||
)
|
||||
|
||||
// Scan ...
|
||||
func (bc *basicController) Scan(artifact *scan.Artifact) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetReport ...
|
||||
func (bc *basicController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetScanLog ...
|
||||
func (bc *basicController) GetScanLog(digest string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
43
src/pkg/scan/scanner/dao/scan/report.go
Normal file
43
src/pkg/scan/scanner/dao/scan/report.go
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scan
|
||||
|
||||
import "time"
|
||||
|
||||
// Report of the scan
|
||||
// Identified by the `digest` and `endpoint_id`
|
||||
type Report struct {
|
||||
ID int64 `orm:"pk;auto;column(id)"`
|
||||
Digest string `orm:"column(digest)"`
|
||||
ReregistrationID string `orm:"column(registration_id)"`
|
||||
JobID string `orm:"column(job_id)"`
|
||||
Status string `orm:"column(status)"`
|
||||
StatusCode int `orm:"column(status_code)"`
|
||||
Report string `orm:"column(report);type(json)"`
|
||||
StartTime time.Time `orm:"column(start_time);auto_now_add;type(datetime)"`
|
||||
EndTime time.Time `orm:"column(end_time);type(datetime)"`
|
||||
}
|
||||
|
||||
// TableName for Report
|
||||
func (r *Report) TableName() string {
|
||||
return "scanner_report"
|
||||
}
|
||||
|
||||
// TableUnique for Report
|
||||
func (r *Report) TableUnique() [][]string {
|
||||
return [][]string{
|
||||
{"digest", "registration_id"},
|
||||
}
|
||||
}
|
120
src/pkg/scan/scanner/dao/scanner/model.go
Normal file
120
src/pkg/scan/scanner/dao/scanner/model.go
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Registration represents a named configuration for invoking a scanner via its adapter.
|
||||
// UUID will be used to track the scanner.Endpoint as unique ID
|
||||
type Registration struct {
|
||||
// Basic information
|
||||
// int64 ID is kept for being aligned with previous DB schema
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"-"`
|
||||
UUID string `orm:"unique;column(uuid)" json:"uuid"`
|
||||
Name string `orm:"unique;column(name);size(128)" json:"name"`
|
||||
Description string `orm:"column(description);null;size(1024)" json:"description"`
|
||||
URL string `orm:"column(url);unique;size(512)" json:"url"`
|
||||
Disabled bool `orm:"column(disabled);default(true)" json:"disabled"`
|
||||
IsDefault bool `orm:"column(is_default);default(false)" json:"is_default"`
|
||||
Health bool `orm:"-" json:"health"`
|
||||
|
||||
// Authentication settings
|
||||
// "None","Basic" and "Bearer" can be supported
|
||||
Auth string `orm:"column(auth);size(16)" json:"auth"`
|
||||
AccessCredential string `orm:"column(access_cred);null;size(512)" json:"access_credential,omitempty"`
|
||||
|
||||
// Http connection settings
|
||||
SkipCertVerify bool `orm:"column(skip_cert_verify);default(false)" json:"skip_certVerify"`
|
||||
|
||||
// Adapter settings
|
||||
Adapter string `orm:"column(adapter);size(128)" json:"adapter"`
|
||||
Vendor string `orm:"column(vendor);size(128)" json:"vendor"`
|
||||
Version string `orm:"column(version);size(32)" json:"version"`
|
||||
|
||||
// Timestamps
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
}
|
||||
|
||||
// TableName for Endpoint
|
||||
func (r *Registration) TableName() string {
|
||||
return "scanner_registration"
|
||||
}
|
||||
|
||||
// FromJSON parses json data
|
||||
func (r *Registration) FromJSON(jsonData string) error {
|
||||
if len(jsonData) == 0 {
|
||||
return errors.New("empty json data to parse")
|
||||
}
|
||||
|
||||
return json.Unmarshal([]byte(jsonData), r)
|
||||
}
|
||||
|
||||
// ToJSON marshals endpoint to JSON data
|
||||
func (r *Registration) ToJSON() (string, error) {
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Validate endpoint
|
||||
func (r *Registration) Validate(checkUUID bool) error {
|
||||
if checkUUID && len(r.UUID) == 0 {
|
||||
return errors.New("malformed endpoint")
|
||||
}
|
||||
|
||||
if len(r.Name) == 0 {
|
||||
return errors.New("missing registration name")
|
||||
}
|
||||
|
||||
err := checkURL(r.URL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scanner registration validate")
|
||||
}
|
||||
|
||||
if len(r.Adapter) == 0 ||
|
||||
len(r.Vendor) == 0 ||
|
||||
len(r.Version) == 0 {
|
||||
return errors.Errorf("missing adapter settings in registration %s:%s", r.Name, r.URL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check the registration URL with url package
|
||||
func checkURL(u string) error {
|
||||
if len(strings.TrimSpace(u)) == 0 {
|
||||
return errors.New("empty url")
|
||||
}
|
||||
|
||||
uri, err := url.Parse(u)
|
||||
if err == nil {
|
||||
if uri.Scheme != "http" && uri.Scheme != "https" {
|
||||
err = errors.New("invalid scheme")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
87
src/pkg/scan/scanner/dao/scanner/model_test.go
Normal file
87
src/pkg/scan/scanner/dao/scanner/model_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// ModelTestSuite tests the utility functions of the model
|
||||
type ModelTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
// TestModel is the entry of the model test suite
|
||||
func TestModel(t *testing.T) {
|
||||
suite.Run(t, new(ModelTestSuite))
|
||||
}
|
||||
|
||||
// TestJSON tests the marshal and unmarshal functions
|
||||
func (suite *ModelTestSuite) TestJSON() {
|
||||
r := &Registration{
|
||||
Name: "forUT",
|
||||
Description: "sample registration",
|
||||
URL: "https://sample.scanner.com",
|
||||
Adapter: "Clair",
|
||||
Version: "0.1.0",
|
||||
Vendor: "Harbor",
|
||||
}
|
||||
|
||||
json, err := r.ToJSON()
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Condition(suite.T(), func() (success bool) {
|
||||
success = len(json) > 0
|
||||
return
|
||||
})
|
||||
|
||||
r2 := &Registration{}
|
||||
err = r2.FromJSON(json)
|
||||
require.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), "forUT", r2.Name)
|
||||
}
|
||||
|
||||
// TestValidate tests the validate function
|
||||
func (suite *ModelTestSuite) TestValidate() {
|
||||
r := &Registration{}
|
||||
|
||||
err := r.Validate(true)
|
||||
require.Error(suite.T(), err)
|
||||
|
||||
r.UUID = "uuid"
|
||||
err = r.Validate(true)
|
||||
require.Error(suite.T(), err)
|
||||
|
||||
r.Name = "forUT"
|
||||
err = r.Validate(true)
|
||||
require.Error(suite.T(), err)
|
||||
|
||||
r.URL = "a.b.c"
|
||||
err = r.Validate(true)
|
||||
require.Error(suite.T(), err)
|
||||
|
||||
r.URL = "http://a.b.c"
|
||||
err = r.Validate(true)
|
||||
require.Error(suite.T(), err)
|
||||
|
||||
r.Adapter = "Clair"
|
||||
r.Vendor = "Harbor"
|
||||
r.Version = "0.1.0"
|
||||
err = r.Validate(true)
|
||||
require.NoError(suite.T(), err)
|
||||
}
|
147
src/pkg/scan/scanner/dao/scanner/registration.go
Normal file
147
src/pkg/scan/scanner/dao/scanner/registration.go
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Registration))
|
||||
}
|
||||
|
||||
// AddRegistration adds a new registration
|
||||
func AddRegistration(r *Registration) (int64, error) {
|
||||
o := dao.GetOrmer()
|
||||
return o.Insert(r)
|
||||
}
|
||||
|
||||
// GetRegistration gets the specified registration
|
||||
func GetRegistration(UUID string) (*Registration, error) {
|
||||
e := &Registration{}
|
||||
|
||||
o := dao.GetOrmer()
|
||||
qs := o.QueryTable(new(Registration))
|
||||
|
||||
if err := qs.Filter("uuid", UUID).One(e); err != nil {
|
||||
if err == orm.ErrNoRows {
|
||||
// Not existing case
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// UpdateRegistration update the specified registration
|
||||
func UpdateRegistration(r *Registration, cols ...string) error {
|
||||
o := dao.GetOrmer()
|
||||
count, err := o.Update(r, cols...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return errors.Errorf("no item with UUID %s is updated", r.UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRegistration deletes the registration with the specified UUID
|
||||
func DeleteRegistration(UUID string) error {
|
||||
o := dao.GetOrmer()
|
||||
qt := o.QueryTable(new(Registration))
|
||||
|
||||
// delete with query way
|
||||
count, err := qt.Filter("uuid", UUID).Delete()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return errors.Errorf("no item with UUID %s is deleted", UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRegistrations lists all the existing registrations
|
||||
func ListRegistrations(query *q.Query) ([]*Registration, error) {
|
||||
o := dao.GetOrmer()
|
||||
qt := o.QueryTable(new(Registration))
|
||||
|
||||
if query != nil {
|
||||
if len(query.Keywords) > 0 {
|
||||
for k, v := range query.Keywords {
|
||||
qt = qt.Filter(fmt.Sprintf("%s__icontains", k), v)
|
||||
}
|
||||
}
|
||||
|
||||
if query.PageNumber > 0 && query.PageSize > 0 {
|
||||
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
|
||||
}
|
||||
}
|
||||
|
||||
l := make([]*Registration, 0)
|
||||
_, err := qt.All(&l)
|
||||
|
||||
return l, err
|
||||
}
|
||||
|
||||
// SetDefaultRegistration sets the specified registration as default one
|
||||
func SetDefaultRegistration(UUID string) error {
|
||||
o := dao.GetOrmer()
|
||||
qt := o.QueryTable(new(Registration))
|
||||
|
||||
_, err := qt.Filter("is_default", true).Update(orm.Params{
|
||||
"is_default": false,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qt2 := o.QueryTable(new(Registration))
|
||||
_, err = qt2.Filter("uuid", UUID).Update(orm.Params{
|
||||
"is_default": true,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDefaultRegistration gets the default registration
|
||||
func GetDefaultRegistration() (*Registration, error) {
|
||||
o := dao.GetOrmer()
|
||||
qt := o.QueryTable(new(Registration))
|
||||
|
||||
e := &Registration{}
|
||||
if err := qt.Filter("is_default", true).One(e); err != nil {
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
144
src/pkg/scan/scanner/dao/scanner/registration_test.go
Normal file
144
src/pkg/scan/scanner/dao/scanner/registration_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// RegistrationDAOTestSuite is test suite of testing registration DAO
|
||||
type RegistrationDAOTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
registrationID string
|
||||
}
|
||||
|
||||
// TestRegistrationDAO is entry of test cases
|
||||
func TestRegistrationDAO(t *testing.T) {
|
||||
suite.Run(t, new(RegistrationDAOTestSuite))
|
||||
}
|
||||
|
||||
// SetupSuite prepare testing env for the suite
|
||||
func (suite *RegistrationDAOTestSuite) SetupSuite() {
|
||||
dao.PrepareTestForPostgresSQL()
|
||||
}
|
||||
|
||||
// SetupTest prepare stuff for test cases
|
||||
func (suite *RegistrationDAOTestSuite) SetupTest() {
|
||||
suite.registrationID = uuid.New().String()
|
||||
r := &Registration{
|
||||
UUID: suite.registrationID,
|
||||
Name: "forUT",
|
||||
Description: "sample registration",
|
||||
URL: "https://sample.scanner.com",
|
||||
Adapter: "Clair",
|
||||
Version: "0.1.0",
|
||||
Vendor: "Harbor",
|
||||
}
|
||||
|
||||
_, err := AddRegistration(r)
|
||||
require.NoError(suite.T(), err, "add new registration")
|
||||
|
||||
}
|
||||
|
||||
// TearDownTest clears all the stuff of test cases
|
||||
func (suite *RegistrationDAOTestSuite) TearDownTest() {
|
||||
err := DeleteRegistration(suite.registrationID)
|
||||
require.NoError(suite.T(), err, "clear registration")
|
||||
}
|
||||
|
||||
// TestGet tests get registration
|
||||
func (suite *RegistrationDAOTestSuite) TestGet() {
|
||||
// Found
|
||||
r, err := GetRegistration(suite.registrationID)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
assert.Equal(suite.T(), r.Name, "forUT")
|
||||
|
||||
// Not found
|
||||
re, err := GetRegistration("not_found")
|
||||
require.NoError(suite.T(), err)
|
||||
require.Nil(suite.T(), re)
|
||||
}
|
||||
|
||||
// TestUpdate tests update registration
|
||||
func (suite *RegistrationDAOTestSuite) TestUpdate() {
|
||||
r, err := GetRegistration(suite.registrationID)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
|
||||
r.Disabled = true
|
||||
r.IsDefault = true
|
||||
r.URL = "http://updated.registration.com"
|
||||
|
||||
err = UpdateRegistration(r)
|
||||
require.NoError(suite.T(), err, "update registration")
|
||||
|
||||
r, err = GetRegistration(suite.registrationID)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
|
||||
assert.Equal(suite.T(), true, r.Disabled)
|
||||
assert.Equal(suite.T(), true, r.IsDefault)
|
||||
assert.Equal(suite.T(), "http://updated.registration.com", r.URL)
|
||||
}
|
||||
|
||||
// TestList tests list registrations
|
||||
func (suite *RegistrationDAOTestSuite) TestList() {
|
||||
// no query
|
||||
l, err := ListRegistrations(nil)
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), 1, len(l))
|
||||
|
||||
// with query and found items
|
||||
keywords := make(map[string]string)
|
||||
keywords["adapter"] = "Clair"
|
||||
l, err = ListRegistrations(&q.Query{
|
||||
PageSize: 5,
|
||||
PageNumber: 1,
|
||||
Keywords: keywords,
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), 1, len(l))
|
||||
|
||||
// With query and not found items
|
||||
keywords["adapter"] = "Micro scanner"
|
||||
l, err = ListRegistrations(&q.Query{
|
||||
Keywords: keywords,
|
||||
})
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), 0, len(l))
|
||||
}
|
||||
|
||||
// TestDefault tests set/get default
|
||||
func (suite *RegistrationDAOTestSuite) TestDefault() {
|
||||
dr, err := GetDefaultRegistration()
|
||||
require.NoError(suite.T(), err, "not found")
|
||||
require.Nil(suite.T(), dr)
|
||||
|
||||
err = SetDefaultRegistration(suite.registrationID)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
dr, err = GetDefaultRegistration()
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), dr)
|
||||
}
|
131
src/pkg/scan/scanner/manager.go
Normal file
131
src/pkg/scan/scanner/manager.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Manager defines the related scanner API endpoints
|
||||
type Manager interface {
|
||||
// List returns a list of currently configured scanner registrations.
|
||||
// Query parameters are optional
|
||||
List(query *q.Query) ([]*scanner.Registration, error)
|
||||
|
||||
// Create creates a new scanner registration with the given data.
|
||||
// Returns the scanner registration identifier.
|
||||
Create(registration *scanner.Registration) (string, error)
|
||||
|
||||
// Get returns the details of the specified scanner registration.
|
||||
Get(registrationUUID string) (*scanner.Registration, error)
|
||||
|
||||
// Update updates the specified scanner registration.
|
||||
Update(registration *scanner.Registration) error
|
||||
|
||||
// Delete deletes the specified scanner registration.
|
||||
Delete(registrationUUID string) error
|
||||
|
||||
// SetAsDefault marks the specified scanner registration as default.
|
||||
// The implementation is supposed to unset any registration previously set as default.
|
||||
SetAsDefault(registrationUUID string) error
|
||||
|
||||
// GetDefault returns the default scanner registration or `nil` if there are no registrations configured.
|
||||
GetDefault() (*scanner.Registration, error)
|
||||
}
|
||||
|
||||
// basicManager is the default implementation of Manager
|
||||
type basicManager struct{}
|
||||
|
||||
// New a basic manager
|
||||
func New() Manager {
|
||||
return &basicManager{}
|
||||
}
|
||||
|
||||
// Create ...
|
||||
func (bm *basicManager) Create(registration *scanner.Registration) (string, error) {
|
||||
if registration == nil {
|
||||
return "", errors.New("nil endpoint to create")
|
||||
}
|
||||
|
||||
// Inject new UUID
|
||||
uid, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "new UUID: create registration")
|
||||
}
|
||||
registration.UUID = uid.String()
|
||||
|
||||
if err := registration.Validate(true); err != nil {
|
||||
return "", errors.Wrap(err, "create registration")
|
||||
}
|
||||
|
||||
if _, err := scanner.AddRegistration(registration); err != nil {
|
||||
return "", errors.Wrap(err, "dao: create registration")
|
||||
}
|
||||
|
||||
return uid.String(), nil
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (bm *basicManager) Get(registrationUUID string) (*scanner.Registration, error) {
|
||||
if len(registrationUUID) == 0 {
|
||||
return nil, errors.New("empty uuid of registration")
|
||||
}
|
||||
|
||||
return scanner.GetRegistration(registrationUUID)
|
||||
}
|
||||
|
||||
// Update ...
|
||||
func (bm *basicManager) Update(registration *scanner.Registration) error {
|
||||
if registration == nil {
|
||||
return errors.New("nil endpoint to update")
|
||||
}
|
||||
|
||||
if err := registration.Validate(true); err != nil {
|
||||
return errors.Wrap(err, "update endpoint")
|
||||
}
|
||||
|
||||
return scanner.UpdateRegistration(registration)
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (bm *basicManager) Delete(registrationUUID string) error {
|
||||
if len(registrationUUID) == 0 {
|
||||
return errors.New("empty UUID to delete")
|
||||
}
|
||||
|
||||
return scanner.DeleteRegistration(registrationUUID)
|
||||
}
|
||||
|
||||
// List ...
|
||||
func (bm *basicManager) List(query *q.Query) ([]*scanner.Registration, error) {
|
||||
return scanner.ListRegistrations(query)
|
||||
}
|
||||
|
||||
// SetAsDefault ...
|
||||
func (bm *basicManager) SetAsDefault(registrationUUID string) error {
|
||||
if len(registrationUUID) == 0 {
|
||||
return errors.New("empty UUID to set default")
|
||||
}
|
||||
|
||||
return scanner.SetDefaultRegistration(registrationUUID)
|
||||
}
|
||||
|
||||
// GetDefault ...
|
||||
func (bm *basicManager) GetDefault() (*scanner.Registration, error) {
|
||||
return scanner.GetDefaultRegistration()
|
||||
}
|
115
src/pkg/scan/scanner/manager_test.go
Normal file
115
src/pkg/scan/scanner/manager_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// BasicManagerTestSuite tests the basic manager
|
||||
type BasicManagerTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
mgr Manager
|
||||
sampleUUID string
|
||||
}
|
||||
|
||||
// TestBasicManager is the entry of BasicManagerTestSuite
|
||||
func TestBasicManager(t *testing.T) {
|
||||
suite.Run(t, new(BasicManagerTestSuite))
|
||||
}
|
||||
|
||||
// SetupSuite prepares env for test suite
|
||||
func (suite *BasicManagerTestSuite) SetupSuite() {
|
||||
dao.PrepareTestForPostgresSQL()
|
||||
|
||||
suite.mgr = New()
|
||||
|
||||
r := &scanner.Registration{
|
||||
Name: "forUT",
|
||||
Description: "sample registration",
|
||||
URL: "https://sample.scanner.com",
|
||||
Adapter: "Clair",
|
||||
Version: "0.1.0",
|
||||
Vendor: "Harbor",
|
||||
}
|
||||
|
||||
uid, err := suite.mgr.Create(r)
|
||||
require.NoError(suite.T(), err)
|
||||
suite.sampleUUID = uid
|
||||
}
|
||||
|
||||
// TearDownSuite clears env for test suite
|
||||
func (suite *BasicManagerTestSuite) TearDownSuite() {
|
||||
err := suite.mgr.Delete(suite.sampleUUID)
|
||||
require.NoError(suite.T(), err, "delete registration")
|
||||
}
|
||||
|
||||
// TestList tests list registrations
|
||||
func (suite *BasicManagerTestSuite) TestList() {
|
||||
m := make(map[string]string, 1)
|
||||
m["name"] = "forUT"
|
||||
|
||||
l, err := suite.mgr.List(&q.Query{
|
||||
PageNumber: 1,
|
||||
PageSize: 10,
|
||||
Keywords: m,
|
||||
})
|
||||
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), 1, len(l))
|
||||
}
|
||||
|
||||
// TestGet tests get registration
|
||||
func (suite *BasicManagerTestSuite) TestGet() {
|
||||
r, err := suite.mgr.Get(suite.sampleUUID)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
assert.Equal(suite.T(), "forUT", r.Name)
|
||||
}
|
||||
|
||||
// TestUpdate tests update registration
|
||||
func (suite *BasicManagerTestSuite) TestUpdate() {
|
||||
r, err := suite.mgr.Get(suite.sampleUUID)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
|
||||
r.URL = "https://updated.com"
|
||||
err = suite.mgr.Update(r)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
r, err = suite.mgr.Get(suite.sampleUUID)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), r)
|
||||
assert.Equal(suite.T(), "https://updated.com", r.URL)
|
||||
}
|
||||
|
||||
// TestDefault tests get/set default registration
|
||||
func (suite *BasicManagerTestSuite) TestDefault() {
|
||||
err := suite.mgr.SetAsDefault(suite.sampleUUID)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
dr, err := suite.mgr.GetDefault()
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotNil(suite.T(), dr)
|
||||
assert.Equal(suite.T(), true, dr.IsDefault)
|
||||
}
|
48
src/pkg/scan/scanner/scan/controller.go
Normal file
48
src/pkg/scan/scanner/scan/controller.go
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scan
|
||||
|
||||
import "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
|
||||
|
||||
// Options object for the scan action
|
||||
type Options struct{}
|
||||
|
||||
// Option for scan action
|
||||
type Option interface {
|
||||
// Apply option to the passing in options
|
||||
Apply(options *Options) error
|
||||
}
|
||||
|
||||
// Controller defines operations for scan controlling
|
||||
type Controller interface {
|
||||
// Scan the given artifact
|
||||
//
|
||||
// Arguments:
|
||||
// artifact *res.Artifact : artifact to be scanned
|
||||
//
|
||||
// Returns:
|
||||
// error : non nil error if any errors occurred
|
||||
Scan(artifact *Artifact, options ...Option) error
|
||||
|
||||
// GetReport gets the reports for the given artifact identified by the digest
|
||||
//
|
||||
// Arguments:
|
||||
// artifact *res.Artifact : the scanned artifact
|
||||
//
|
||||
// Returns:
|
||||
// []*scan.Report : scan results by different scanner vendors
|
||||
// error : non nil error if any errors occurred
|
||||
GetReport(artifact *Artifact) ([]*scan.Report, error)
|
||||
}
|
46
src/pkg/scan/scanner/scan/models.go
Normal file
46
src/pkg/scan/scanner/scan/models.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package scan
|
||||
|
||||
// Artifact represents an artifact stored in Registry.
|
||||
type Artifact struct {
|
||||
// The full name of a Harbor repository containing the artifact, including the namespace.
|
||||
// For example, `library/oracle/nosql`.
|
||||
Repository string
|
||||
// The artifact's digest, consisting of an algorithm and hex portion.
|
||||
// For example, `sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b`,
|
||||
// represents sha256 based digest.
|
||||
Digest string
|
||||
// The mime type of the scanned artifact
|
||||
MimeType string
|
||||
}
|
||||
|
||||
// Registry represents Registry connection settings.
|
||||
type Registry struct {
|
||||
// A base URL of the Docker Registry v2 API exposed by Harbor.
|
||||
URL string
|
||||
// An optional value of the HTTP Authorization header sent with each request to the Docker Registry v2 API.
|
||||
// For example, `Bearer: JWTTOKENGOESHERE`.
|
||||
Authorization string
|
||||
}
|
||||
|
||||
// Request represents a structure that is sent to a Scanner Adapter to initiate artifact scanning.
|
||||
// Conducts all the details required to pull the artifact from a Harbor registry.
|
||||
type Request struct {
|
||||
// Connection settings for the Docker Registry v2 API exposed by Harbor.
|
||||
Registry *Registry
|
||||
// Artifact to be scanned.
|
||||
Artifact *Artifact
|
||||
}
|
@ -26,6 +26,7 @@
|
||||
"node_modules/@clr/ui/clr-ui.min.css",
|
||||
"node_modules/swagger-ui/dist/swagger-ui.css",
|
||||
"node_modules/prismjs/themes/prism-solarizedlight.css",
|
||||
"src/global.scss",
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
|
@ -4,11 +4,6 @@
|
||||
"deleteDestPath": false,
|
||||
"lib": {
|
||||
"entryFile": "index.ts",
|
||||
"externals": {
|
||||
"@ngx-translate/core": "ngx-translate-core",
|
||||
"@ngx-translate/core/index": "ngx-translate-core",
|
||||
"ngx-markdown": "ngx-markdown"
|
||||
},
|
||||
"umdModuleIds": {
|
||||
"@clr/angular" : "angular",
|
||||
"ngx-markdown" : "ngxMarkdown",
|
||||
|
@ -3,11 +3,6 @@
|
||||
"dest": "./dist",
|
||||
"lib": {
|
||||
"entryFile": "index.ts",
|
||||
"externals": {
|
||||
"@ngx-translate/core": "ngx-translate-core",
|
||||
"@ngx-translate/core/index": "ngx-translate-core",
|
||||
"ngx-markdown": "ngx-markdown"
|
||||
},
|
||||
"umdModuleIds": {
|
||||
"@clr/angular" : "angular",
|
||||
"ngx-markdown" : "ngxMarkdown",
|
||||
|
5
src/portal/lib/package-lock.json
generated
Normal file
5
src/portal/lib/package-lock.json
generated
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@harbor/ui",
|
||||
"version": "1.10.0",
|
||||
"lockfileVersion": 1
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@harbor/ui",
|
||||
"version": "1.9.0",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular7",
|
||||
"version": "1.10.0",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular8",
|
||||
"author": "CNCF",
|
||||
"module": "index.js",
|
||||
"main": "bundles/harborui.umd.min.js",
|
||||
@ -19,26 +19,26 @@
|
||||
},
|
||||
"homepage": "https://github.com/vmware/harbor#readme",
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "^7.1.3",
|
||||
"@angular/common": "^7.1.3",
|
||||
"@angular/compiler": "^7.1.3",
|
||||
"@angular/core": "^7.1.3",
|
||||
"@angular/forms": "^7.1.3",
|
||||
"@angular/http": "^7.1.3",
|
||||
"@angular/platform-browser": "^7.1.3",
|
||||
"@angular/platform-browser-dynamic": "^7.1.3",
|
||||
"@angular/router": "^7.1.3",
|
||||
"@angular/animations": "^8.2.0",
|
||||
"@angular/common": "^8.2.0",
|
||||
"@angular/compiler": "^8.2.0",
|
||||
"@angular/core": "^8.2.0",
|
||||
"@angular/forms": "^8.2.0",
|
||||
"@angular/http": "^8.2.0",
|
||||
"@angular/platform-browser": "^8.2.0",
|
||||
"@angular/platform-browser-dynamic": "^8.2.0",
|
||||
"@angular/router": "^8.2.0",
|
||||
"@ngx-translate/core": "^10.0.2",
|
||||
"@ngx-translate/http-loader": "^3.0.1",
|
||||
"@webcomponents/custom-elements": "^1.1.3",
|
||||
"@clr/angular": "^1.0.0",
|
||||
"@clr/ui": "^1.0.0",
|
||||
"@clr/icons": "^1.0.0",
|
||||
"@clr/angular": "^2.1.0",
|
||||
"@clr/icons": "^2.1.0",
|
||||
"@clr/ui": "^2.1.0",
|
||||
"core-js": "^2.5.4",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
"ngx-markdown": "^6.2.0",
|
||||
"ngx-markdown": "^8.1.0",
|
||||
"rxjs": "^6.3.3",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"web-animations-js": "^2.2.1",
|
||||
|
@ -100,6 +100,7 @@ export class Configuration {
|
||||
oidc_scope?: StringValueItem;
|
||||
count_per_project: NumberValueItem;
|
||||
storage_per_project: NumberValueItem;
|
||||
cfg_expiration: NumberValueItem;
|
||||
public constructor() {
|
||||
this.auth_mode = new StringValueItem("db_auth", true);
|
||||
this.project_creation_restriction = new StringValueItem("everyone", true);
|
||||
|
@ -9,6 +9,7 @@ import { GcViewModelFactory } from './gc.viewmodel.factory';
|
||||
import { CronScheduleComponent } from '../../cron-schedule/cron-schedule.component';
|
||||
import { CronTooltipComponent } from "../../cron-schedule/cron-tooltip/cron-tooltip.component";
|
||||
import { of } from 'rxjs';
|
||||
import { GcJobData } from './gcLog';
|
||||
|
||||
describe('GcComponent', () => {
|
||||
let component: GcComponent;
|
||||
@ -18,13 +19,17 @@ describe('GcComponent', () => {
|
||||
systemInfoEndpoint: "/api/system/gc"
|
||||
};
|
||||
let mockSchedule = [];
|
||||
let mockJobs = [
|
||||
let mockJobs: GcJobData[] = [
|
||||
{
|
||||
id: 22222,
|
||||
schedule: null,
|
||||
job_status: 'string',
|
||||
creation_time: new Date(),
|
||||
update_time: new Date(),
|
||||
creation_time: new Date().toDateString(),
|
||||
update_time: new Date().toDateString(),
|
||||
job_name: 'string',
|
||||
job_kind: 'string',
|
||||
job_uuid: 'string',
|
||||
delete: false
|
||||
}
|
||||
];
|
||||
let spySchedule: jasmine.Spy;
|
||||
|
@ -32,7 +32,7 @@ export class GcComponent implements OnInit {
|
||||
getText = 'CONFIG.GC';
|
||||
getLabelCurrent = 'GC.CURRENT_SCHEDULE';
|
||||
@Output() loadingGcStatus = new EventEmitter<boolean>();
|
||||
@ViewChild(CronScheduleComponent)
|
||||
@ViewChild(CronScheduleComponent, {static: false})
|
||||
CronScheduleComponent: CronScheduleComponent;
|
||||
constructor(
|
||||
private gcRepoService: GcRepoService,
|
||||
|
@ -4,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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -38,10 +38,10 @@ export class EditProjectQuotasComponent implements OnInit {
|
||||
staticBackdrop = true;
|
||||
closable = false;
|
||||
quotaForm: NgForm;
|
||||
@ViewChild(InlineAlertComponent)
|
||||
@ViewChild(InlineAlertComponent, {static: false})
|
||||
inlineAlert: InlineAlertComponent;
|
||||
|
||||
@ViewChild('quotaForm')
|
||||
@ViewChild('quotaForm', {static: true})
|
||||
currentForm: NgForm;
|
||||
@Output() confirmAction = new EventEmitter();
|
||||
quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT;
|
||||
|
@ -40,3 +40,19 @@
|
||||
margin-top: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.progress {
|
||||
&.warning>progress {
|
||||
color: orange;
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ const QuotaType = 'project';
|
||||
export class ProjectQuotasComponent implements OnChanges {
|
||||
|
||||
config: Configuration = new Configuration();
|
||||
@ViewChild('editProjectQuotas')
|
||||
@ViewChild('editProjectQuotas', {static: false})
|
||||
editQuotaDialog: EditProjectQuotasComponent;
|
||||
loading = true;
|
||||
quotaHardLimitValue: QuotaHardLimitInterface;
|
||||
|
@ -29,10 +29,10 @@ export class RegistryConfigComponent implements OnInit {
|
||||
|
||||
@Input() hasAdminRole: boolean = false;
|
||||
|
||||
@ViewChild("systemSettings") systemSettings: SystemSettingsComponent;
|
||||
@ViewChild("vulnerabilityConfig") vulnerabilityCfg: VulnerabilityConfigComponent;
|
||||
@ViewChild("gc") gc: GcComponent;
|
||||
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
|
||||
@ViewChild("systemSettings", {static: false}) systemSettings: SystemSettingsComponent;
|
||||
@ViewChild("vulnerabilityConfig", {static: false}) vulnerabilityCfg: VulnerabilityConfigComponent;
|
||||
@ViewChild("gc", {static: false}) gc: GcComponent;
|
||||
@ViewChild("cfgConfirmationDialog", {static: false}) confirmationDlg: ConfirmationDialogComponent;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigurationService,
|
||||
|
@ -22,7 +22,7 @@ export class ReplicationConfigComponent {
|
||||
|
||||
@Input() showSubTitle: boolean = false;
|
||||
|
||||
@ViewChild("replicationConfigFrom") replicationConfigForm: NgForm;
|
||||
@ViewChild("replicationConfigFrom", { static: false }) replicationConfigForm: NgForm;
|
||||
|
||||
get editable(): boolean {
|
||||
return this.replicationConfig &&
|
||||
|
@ -1,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>
|
@ -3,12 +3,43 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.create-tooltip {
|
||||
top: -1;
|
||||
.clr-form-horizontal {
|
||||
.clr-form-control {
|
||||
& >.clr-control-label {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
.flex-direction-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.bottom-line {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.expire-data {
|
||||
min-width: 12.5rem;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
.pl-05 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.read-tooltip {
|
||||
top: -7px;
|
||||
.d-f {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.font-size-13 {
|
||||
font-size: 13px;
|
||||
}
|
||||
.mt-05 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -34,18 +65,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.width-70per {
|
||||
width: 70%;
|
||||
.width-90per {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.none {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.underline {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.color-0079bb {
|
||||
color: #0079bb;
|
||||
}
|
||||
|
@ -66,9 +66,9 @@ export class SystemSettingsComponent implements OnChanges, OnInit {
|
||||
@Input() hasCAFile: boolean = false;
|
||||
@Input() withAdmiral = false;
|
||||
|
||||
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
|
||||
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
|
||||
@ViewChild('dateInput') dateInput: ElementRef;
|
||||
@ViewChild("systemConfigFrom", {static: false}) systemSettingsForm: NgForm;
|
||||
@ViewChild("cfgConfirmationDialog", {static: false}) confirmationDlg: ConfirmationDialogComponent;
|
||||
@ViewChild('dateInput', {static: false}) dateInput: ElementRef;
|
||||
|
||||
get editable(): boolean {
|
||||
return this.systemSettings &&
|
||||
|
@ -34,7 +34,7 @@ export class VulnerabilityConfigComponent implements OnInit {
|
||||
openState: boolean = false;
|
||||
getLabelCurrent: string;
|
||||
|
||||
@ViewChild(CronScheduleComponent)
|
||||
@ViewChild(CronScheduleComponent, {static: false})
|
||||
CronScheduleComponent: CronScheduleComponent;
|
||||
|
||||
@Input()
|
||||
@ -109,7 +109,7 @@ export class VulnerabilityConfigComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
}
|
||||
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
|
||||
@ViewChild("systemConfigFrom", {static: false}) systemSettingsForm: NgForm;
|
||||
|
||||
get isValid(): boolean {
|
||||
return this.systemSettingsForm && this.systemSettingsForm.valid;
|
||||
|
@ -9,98 +9,96 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form #targetForm="ngForm">
|
||||
<section class="form-block">
|
||||
<!-- 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>
|
@ -63,13 +63,13 @@ export class CreateEditEndpointComponent
|
||||
selectedType: string;
|
||||
initVal: Endpoint;
|
||||
targetForm: NgForm;
|
||||
@ViewChild("targetForm") currentForm: NgForm;
|
||||
@ViewChild("targetForm", {static: false}) currentForm: NgForm;
|
||||
targetEndpoint;
|
||||
testOngoing: boolean;
|
||||
onGoing: boolean;
|
||||
endpointId: number | string;
|
||||
|
||||
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
|
||||
@ViewChild(InlineAlertComponent, {static: false}) inlineAlert: InlineAlertComponent;
|
||||
|
||||
@Output() reload = new EventEmitter<boolean>();
|
||||
|
||||
|
@ -3,13 +3,13 @@
|
||||
<section>
|
||||
<label>
|
||||
<label for="name">{{'LABEL.LABEL_NAME' | translate}}</label>
|
||||
<label aria-haspopup="true" role="tooltip" [class.invalid]="isLabelNameExist"
|
||||
class="tooltip tooltip-validation tooltip-md tooltip-bottom-left">
|
||||
<input type="text" id="name" name="name" required size="20" autocomplete="off"
|
||||
<label class="clr-control-container" [class.clr-error]="isLabelNameExist">
|
||||
<input clrInput type="text" id="name" name="name" required size="20" autocomplete="off"
|
||||
[(ngModel)]="labelModel.name" #name="ngModel" (keyup)="existValid(labelModel.name)">
|
||||
<span class="tooltip-content">
|
||||
{{'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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ describe("CreateEditLabelComponent (inline template)", () => {
|
||||
labelService = fixture.debugElement.injector.get(LabelService);
|
||||
|
||||
spy = spyOn(labelService, "getLabels").and.returnValue(
|
||||
of(mockOneData)
|
||||
of([mockOneData])
|
||||
);
|
||||
spyOne = spyOn(labelService, "createLabel").and.returnValue(
|
||||
of(mockOneData)
|
||||
|
@ -52,7 +52,7 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
|
||||
nameChecker = new Subject<string>();
|
||||
|
||||
labelForm: NgForm;
|
||||
@ViewChild("labelForm") currentForm: NgForm;
|
||||
@ViewChild("labelForm", {static: true}) currentForm: NgForm;
|
||||
|
||||
@Input() projectId: number;
|
||||
@Input() scope: string;
|
||||
@ -71,7 +71,7 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
|
||||
this.isLabelNameExist = false;
|
||||
if (targets && targets.length) {
|
||||
if (targets.find((target) => {
|
||||
return target.name === name;
|
||||
return target.name === name && target.id !== this.labelId;
|
||||
})) {
|
||||
this.isLabelNameExist = true;
|
||||
}
|
||||
|
@ -2,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>
|
||||
|
@ -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;
|
||||
}
|
@ -75,7 +75,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
@Output() goToRegistry = new EventEmitter<any>();
|
||||
@Output() reload = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
|
||||
@ViewChild(InlineAlertComponent, {static: true}) inlineAlert: InlineAlertComponent;
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private repService: ReplicationService,
|
||||
|
@ -23,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 }}
|
||||
|
@ -17,7 +17,7 @@ export class DatePickerComponent implements OnChanges {
|
||||
@Input() dateInput: string;
|
||||
@Input() oneDayOffset: boolean;
|
||||
|
||||
@ViewChild("searchTime") searchTime: NgModel;
|
||||
@ViewChild("searchTime", {static: true}) searchTime: NgModel;
|
||||
|
||||
@Output() search = new EventEmitter<string>();
|
||||
|
||||
|
@ -142,7 +142,7 @@ describe("EndpointComponent (inline template)", () => {
|
||||
spyOnRules = spyOn(
|
||||
endpointService,
|
||||
"getEndpointWithReplicationRules"
|
||||
).and.returnValue([]);
|
||||
).and.returnValue(of([]));
|
||||
spyOne = spyOn(endpointService, "getEndpoint").and.returnValue(
|
||||
of(mockOne[0])
|
||||
);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user