mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-04 15:08:21 +01:00
Merge pull request #7433 from goharbor/replication_ng
Merge the replication ng branch to master
This commit is contained in:
commit
16f97326ad
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
harbor
|
|
||||||
!/contrib/helm/harbor
|
!/contrib/helm/harbor
|
||||||
|
|
||||||
make/docker-compose.yml
|
make/docker-compose.yml
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -51,3 +51,107 @@ WHERE j.policy_id = p.id AND p.deleted = TRUE;
|
|||||||
/*delete replication policy which has been marked as "deleted"*/
|
/*delete replication policy which has been marked as "deleted"*/
|
||||||
DELETE FROM replication_policy AS p
|
DELETE FROM replication_policy AS p
|
||||||
WHERE p.deleted = TRUE;
|
WHERE p.deleted = TRUE;
|
||||||
|
|
||||||
|
/*upgrade the replication_target to registry*/
|
||||||
|
DROP TRIGGER replication_target_update_time_at_modtime ON replication_target;
|
||||||
|
ALTER TABLE replication_target RENAME TO registry;
|
||||||
|
ALTER TABLE registry ALTER COLUMN url TYPE varchar(256);
|
||||||
|
ALTER TABLE registry ADD COLUMN credential_type varchar(16);
|
||||||
|
ALTER TABLE registry RENAME COLUMN username TO access_key;
|
||||||
|
ALTER TABLE registry RENAME COLUMN password TO access_secret;
|
||||||
|
ALTER TABLE registry ALTER COLUMN access_secret TYPE varchar(1024);
|
||||||
|
ALTER TABLE registry ADD COLUMN type varchar(32);
|
||||||
|
ALTER TABLE registry DROP COLUMN target_type;
|
||||||
|
ALTER TABLE registry ADD COLUMN description text;
|
||||||
|
ALTER TABLE registry ADD COLUMN health varchar(16);
|
||||||
|
UPDATE registry SET type='harbor';
|
||||||
|
UPDATE registry SET credential_type='basic';
|
||||||
|
|
||||||
|
/*upgrade the replication_policy*/
|
||||||
|
ALTER TABLE replication_policy ADD COLUMN creator varchar(256);
|
||||||
|
ALTER TABLE replication_policy ADD COLUMN src_registry_id int;
|
||||||
|
/*The predefined filters will be cleared and replaced by "project_name/"+double star.
|
||||||
|
if harbor is integrated with the external project service, we cannot get the project name by ID,
|
||||||
|
which means the repilcation policy will match all resources.*/
|
||||||
|
UPDATE replication_policy r SET filters=(SELECT CONCAT('[{"type":"name","value":"', p.name,'/**"}]') FROM project p WHERE p.project_id=r.project_id);
|
||||||
|
ALTER TABLE replication_policy RENAME COLUMN target_id TO dest_registry_id;
|
||||||
|
ALTER TABLE replication_policy ALTER COLUMN dest_registry_id DROP NOT NULL;
|
||||||
|
ALTER TABLE replication_policy ADD COLUMN dest_namespace varchar(256);
|
||||||
|
ALTER TABLE replication_policy ADD COLUMN override boolean;
|
||||||
|
ALTER TABLE replication_policy DROP COLUMN project_id;
|
||||||
|
ALTER TABLE replication_policy RENAME COLUMN cron_str TO trigger;
|
||||||
|
|
||||||
|
DROP TRIGGER replication_immediate_trigger_update_time_at_modtime ON replication_immediate_trigger;
|
||||||
|
DROP TABLE replication_immediate_trigger;
|
||||||
|
|
||||||
|
create table replication_execution (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
policy_id int NOT NULL,
|
||||||
|
status varchar(32),
|
||||||
|
/*the status text may contain error message whose length is very long*/
|
||||||
|
status_text text,
|
||||||
|
total int NOT NULL DEFAULT 0,
|
||||||
|
failed int NOT NULL DEFAULT 0,
|
||||||
|
succeed int NOT NULL DEFAULT 0,
|
||||||
|
in_progress int NOT NULL DEFAULT 0,
|
||||||
|
stopped int NOT NULL DEFAULT 0,
|
||||||
|
trigger varchar(64),
|
||||||
|
start_time timestamp default CURRENT_TIMESTAMP,
|
||||||
|
end_time timestamp NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX execution_policy ON replication_execution (policy_id);
|
||||||
|
|
||||||
|
create table replication_task (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
execution_id int NOT NULL,
|
||||||
|
resource_type varchar(64),
|
||||||
|
src_resource varchar(256),
|
||||||
|
dst_resource varchar(256),
|
||||||
|
operation varchar(32),
|
||||||
|
job_id varchar(64),
|
||||||
|
status varchar(32),
|
||||||
|
start_time timestamp default CURRENT_TIMESTAMP,
|
||||||
|
end_time timestamp NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX task_execution ON replication_task (execution_id);
|
||||||
|
|
||||||
|
|
||||||
|
/*migrate each replication_job record to one replication_execution and one replication_task record*/
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
job RECORD;
|
||||||
|
execid integer;
|
||||||
|
BEGIN
|
||||||
|
FOR job IN SELECT * FROM replication_job WHERE operation != 'schedule'
|
||||||
|
LOOP
|
||||||
|
/*insert one execution record*/
|
||||||
|
INSERT INTO replication_execution (policy_id, start_time) VALUES (job.policy_id, job.creation_time) RETURNING id INTO execid;
|
||||||
|
/*insert one task record
|
||||||
|
doesn't record the tags info in "src_resource" and "dst_resource" as the length
|
||||||
|
of the tags may longer than the capability of the column*/
|
||||||
|
INSERT INTO replication_task (execution_id, resource_type, src_resource, dst_resource, operation, job_id, status, start_time, end_time)
|
||||||
|
VALUES (execid, 'image', job.repository, job.repository, job.operation, job.job_uuid, job.status, job.creation_time, job.update_time);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
UPDATE replication_task SET status='Pending' WHERE status='pending';
|
||||||
|
UPDATE replication_task SET status='InProgress' WHERE status='scheduled';
|
||||||
|
UPDATE replication_task SET status='InProgress' WHERE status='running';
|
||||||
|
UPDATE replication_task SET status='Failed' WHERE status='error';
|
||||||
|
UPDATE replication_task SET status='Succeed' WHERE status='finished';
|
||||||
|
UPDATE replication_task SET operation='copy' WHERE operation='transfer';
|
||||||
|
UPDATE replication_task SET operation='deletion' WHERE operation='delete';
|
||||||
|
|
||||||
|
/*upgrade the replication_job to replication_schedule_job*/
|
||||||
|
DELETE FROM replication_job WHERE operation != 'schedule';
|
||||||
|
ALTER TABLE replication_job RENAME COLUMN job_uuid TO job_id;
|
||||||
|
ALTER TABLE replication_job DROP COLUMN repository;
|
||||||
|
ALTER TABLE replication_job DROP COLUMN operation;
|
||||||
|
ALTER TABLE replication_job DROP COLUMN tags;
|
||||||
|
ALTER TABLE replication_job DROP COLUMN op_uuid;
|
||||||
|
DROP INDEX policy;
|
||||||
|
DROP INDEX poid_uptime;
|
||||||
|
DROP INDEX poid_status;
|
||||||
|
DROP TRIGGER replication_job_update_time_at_modtime ON replication_job;
|
||||||
|
ALTER TABLE replication_job RENAME TO replication_schedule_job;
|
||||||
|
17
src/Gopkg.lock
generated
17
src/Gopkg.lock
generated
@ -81,6 +81,14 @@
|
|||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "e87155e8f0c05bf323d0b13470e1b97af0cb5652"
|
revision = "e87155e8f0c05bf323d0b13470e1b97af0cb5652"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:2aaf2cc045d0219bba79655e4df795b973168c310574669cb75786684f7287d3"
|
||||||
|
name = "github.com/bmatcuk/doublestar"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "UT"
|
||||||
|
revision = "85a78806aa1b4707d1dbace9be592cf1ece91ab3"
|
||||||
|
version = "v1.1.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:76ca0dfcbf951d1868c7449453981dba9e1f79034706d1500a5a785000f5f222"
|
digest = "1:76ca0dfcbf951d1868c7449453981dba9e1f79034706d1500a5a785000f5f222"
|
||||||
name = "github.com/casbin/casbin"
|
name = "github.com/casbin/casbin"
|
||||||
@ -651,11 +659,10 @@
|
|||||||
revision = "f534d624797b270e5e46104dc7e2c2d61edbb85d"
|
revision = "f534d624797b270e5e46104dc7e2c2d61edbb85d"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:b2a0bdcfc59bed6a64d3ade946f9bf807f8fcd105892d940a008b0b2816babe5"
|
digest = "1:131682c26796b64f0abb77ac3d85525712706fde0b085aaa7b6d10b4398167cc"
|
||||||
name = "k8s.io/client-go"
|
name = "k8s.io/client-go"
|
||||||
packages = [
|
packages = [
|
||||||
"kubernetes/scheme",
|
"kubernetes/scheme",
|
||||||
"kubernetes/typed/authentication/v1beta1",
|
|
||||||
"pkg/apis/clientauthentication",
|
"pkg/apis/clientauthentication",
|
||||||
"pkg/apis/clientauthentication/v1alpha1",
|
"pkg/apis/clientauthentication/v1alpha1",
|
||||||
"pkg/apis/clientauthentication/v1beta1",
|
"pkg/apis/clientauthentication/v1beta1",
|
||||||
@ -714,6 +721,7 @@
|
|||||||
"github.com/astaxie/beego/session/redis",
|
"github.com/astaxie/beego/session/redis",
|
||||||
"github.com/astaxie/beego/validation",
|
"github.com/astaxie/beego/validation",
|
||||||
"github.com/beego/i18n",
|
"github.com/beego/i18n",
|
||||||
|
"github.com/bmatcuk/doublestar",
|
||||||
"github.com/casbin/casbin",
|
"github.com/casbin/casbin",
|
||||||
"github.com/casbin/casbin/model",
|
"github.com/casbin/casbin/model",
|
||||||
"github.com/casbin/casbin/persist",
|
"github.com/casbin/casbin/persist",
|
||||||
@ -729,7 +737,6 @@
|
|||||||
"github.com/docker/distribution/reference",
|
"github.com/docker/distribution/reference",
|
||||||
"github.com/docker/distribution/registry/auth/token",
|
"github.com/docker/distribution/registry/auth/token",
|
||||||
"github.com/docker/distribution/registry/client/auth/challenge",
|
"github.com/docker/distribution/registry/client/auth/challenge",
|
||||||
"github.com/docker/distribution/uuid",
|
|
||||||
"github.com/docker/libtrust",
|
"github.com/docker/libtrust",
|
||||||
"github.com/docker/notary",
|
"github.com/docker/notary",
|
||||||
"github.com/docker/notary/client",
|
"github.com/docker/notary/client",
|
||||||
@ -760,7 +767,9 @@
|
|||||||
"gopkg.in/yaml.v2",
|
"gopkg.in/yaml.v2",
|
||||||
"k8s.io/api/authentication/v1beta1",
|
"k8s.io/api/authentication/v1beta1",
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1",
|
"k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||||
"k8s.io/client-go/kubernetes/typed/authentication/v1beta1",
|
"k8s.io/apimachinery/pkg/runtime/schema",
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer",
|
||||||
|
"k8s.io/client-go/kubernetes/scheme",
|
||||||
"k8s.io/client-go/rest",
|
"k8s.io/client-go/rest",
|
||||||
"k8s.io/helm/cmd/helm/search",
|
"k8s.io/helm/cmd/helm/search",
|
||||||
"k8s.io/helm/pkg/chartutil",
|
"k8s.io/helm/pkg/chartutil",
|
||||||
|
@ -123,3 +123,7 @@ ignored = ["github.com/goharbor/harbor/tests*"]
|
|||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "k8s.io/api"
|
name = "k8s.io/api"
|
||||||
version = "kubernetes-1.13.4"
|
version = "kubernetes-1.13.4"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/bmatcuk/doublestar"
|
||||||
|
version = "1.1.1"
|
||||||
|
@ -7,7 +7,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ghodss/yaml"
|
"github.com/ghodss/yaml"
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
helm_repo "k8s.io/helm/pkg/repo"
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
|
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListCharts gets the chart list under the namespace
|
// ListCharts gets the chart list under the namespace
|
||||||
@ -63,7 +70,35 @@ func (c *Controller) DeleteChartVersion(namespace, chartName, version string) er
|
|||||||
|
|
||||||
url := fmt.Sprintf("%s/%s/%s", c.APIPrefix(namespace), chartName, version)
|
url := fmt.Sprintf("%s/%s/%s", c.APIPrefix(namespace), chartName, version)
|
||||||
|
|
||||||
return c.apiClient.DeleteContent(url)
|
err := c.apiClient.DeleteContent(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send notification to replication handler
|
||||||
|
// Todo: it used as the replacement of webhook, will be removed when webhook to be introduced.
|
||||||
|
if os.Getenv("UTTEST") != "true" {
|
||||||
|
go func() {
|
||||||
|
e := &rep_event.Event{
|
||||||
|
Type: rep_event.EventTypeChartDelete,
|
||||||
|
Resource: &model.Resource{
|
||||||
|
Type: model.ResourceTypeChart,
|
||||||
|
Deleted: true,
|
||||||
|
Metadata: &model.ResourceMetadata{
|
||||||
|
Repository: &model.Repository{
|
||||||
|
Name: fmt.Sprintf("%s/%s", namespace, chartName),
|
||||||
|
},
|
||||||
|
Vtags: []string{version},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := replication.EventHandler.Handle(e); err != nil {
|
||||||
|
log.Errorf("failed to handle event: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChartVersion returns the summary of the specified chart version.
|
// GetChartVersion returns the summary of the specified chart version.
|
||||||
|
@ -12,6 +12,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
|
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -80,6 +86,36 @@ func director(target *url.URL, cred *Credential, req *http.Request) {
|
|||||||
|
|
||||||
// Modify the http response
|
// Modify the http response
|
||||||
func modifyResponse(res *http.Response) error {
|
func modifyResponse(res *http.Response) error {
|
||||||
|
// Upload chart success, then to the notification to replication handler
|
||||||
|
if res.StatusCode == http.StatusCreated {
|
||||||
|
// 201 and has chart_upload(namespace-repository-version) context
|
||||||
|
// means this response is for uploading chart success.
|
||||||
|
chartUpload := res.Request.Context().Value(common.ChartUploadCtxKey).(string)
|
||||||
|
if chartUpload != "" {
|
||||||
|
chartUploadSplitted := strings.Split(chartUpload, ":")
|
||||||
|
if len(chartUploadSplitted) == 3 {
|
||||||
|
// Todo: it used as the replacement of webhook, will be removed when webhook to be introduced.
|
||||||
|
go func() {
|
||||||
|
e := &rep_event.Event{
|
||||||
|
Type: rep_event.EventTypeChartUpload,
|
||||||
|
Resource: &model.Resource{
|
||||||
|
Type: model.ResourceTypeChart,
|
||||||
|
Metadata: &model.ResourceMetadata{
|
||||||
|
Repository: &model.Repository{
|
||||||
|
Name: fmt.Sprintf("%s/%s", chartUploadSplitted[0], chartUploadSplitted[1]),
|
||||||
|
},
|
||||||
|
Vtags: []string{chartUploadSplitted[2]},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := replication.EventHandler.Handle(e); err != nil {
|
||||||
|
hlog.Errorf("failed to handle event: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Accept cases
|
// Accept cases
|
||||||
// Success or redirect
|
// Success or redirect
|
||||||
if res.StatusCode >= http.StatusOK && res.StatusCode <= http.StatusTemporaryRedirect {
|
if res.StatusCode >= http.StatusOK && res.StatusCode <= http.StatusTemporaryRedirect {
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
package common
|
package common
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
// const variables
|
// const variables
|
||||||
const (
|
const (
|
||||||
DBAuth = "db_auth"
|
DBAuth = "db_auth"
|
||||||
@ -136,4 +138,6 @@ const (
|
|||||||
RobotTokenDuration = "robot_token_duration"
|
RobotTokenDuration = "robot_token_duration"
|
||||||
|
|
||||||
OIDCCallbackPath = "/c/oidc/callback"
|
OIDCCallbackPath = "/c/oidc/callback"
|
||||||
|
|
||||||
|
ChartUploadCtxKey = contextKey("chart_upload")
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func execUpdate(o orm.Ormer, sql string, params ...interface{}) error {
|
func execUpdate(o orm.Ormer, sql string, params ...interface{}) error {
|
||||||
@ -104,16 +103,11 @@ func cleanByUser(username string) {
|
|||||||
o.Rollback()
|
o.Rollback()
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = execUpdate(o, `delete from replication_job where id < 99`)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
err = execUpdate(o, `delete from replication_policy where id < 99`)
|
err = execUpdate(o, `delete from replication_policy where id < 99`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
err = execUpdate(o, `delete from replication_target where id < 99`)
|
err = execUpdate(o, `delete from registry where id < 99`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
@ -165,8 +159,8 @@ func testForAll(m *testing.M) int {
|
|||||||
func clearAll() {
|
func clearAll() {
|
||||||
tables := []string{"project_member",
|
tables := []string{"project_member",
|
||||||
"project_metadata", "access_log", "repository", "replication_policy",
|
"project_metadata", "access_log", "repository", "replication_policy",
|
||||||
"replication_target", "replication_job", "replication_immediate_trigger", "img_scan_job",
|
"registry", "replication_execution", "replication_task", "img_scan_job",
|
||||||
"img_scan_overview", "clair_vuln_timestamp", "project", "harbor_user"}
|
"replication_schedule_job", "img_scan_overview", "clair_vuln_timestamp", "project", "harbor_user"}
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
if err := ClearTable(t); err != nil {
|
if err := ClearTable(t); err != nil {
|
||||||
log.Errorf("Failed to clear table: %s,error: %v", t, err)
|
log.Errorf("Failed to clear table: %s,error: %v", t, err)
|
||||||
@ -675,439 +669,6 @@ func TestChangeUserProfile(t *testing.T) {
|
|||||||
|
|
||||||
var targetID, policyID, policyID2, policyID3, jobID, jobID2, jobID3 int64
|
var targetID, policyID, policyID2, policyID3, jobID, jobID2, jobID3 int64
|
||||||
|
|
||||||
func TestAddRepTarget(t *testing.T) {
|
|
||||||
target := models.RepTarget{
|
|
||||||
Name: "test",
|
|
||||||
URL: "127.0.0.1:5000",
|
|
||||||
Username: "admin",
|
|
||||||
Password: "admin",
|
|
||||||
}
|
|
||||||
// _, err := AddRepTarget(target)
|
|
||||||
id, err := AddRepTarget(target)
|
|
||||||
t.Logf("added target, id: %d", id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in AddRepTarget: %v", err)
|
|
||||||
} else {
|
|
||||||
targetID = id
|
|
||||||
}
|
|
||||||
id2 := id + 99
|
|
||||||
tgt, err := GetRepTarget(id2)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, id2)
|
|
||||||
}
|
|
||||||
if tgt != nil {
|
|
||||||
t.Errorf("There should not be a target with id: %d", id2)
|
|
||||||
}
|
|
||||||
tgt, err = GetRepTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, id)
|
|
||||||
}
|
|
||||||
if tgt == nil {
|
|
||||||
t.Errorf("Unable to find a target with id: %d", id)
|
|
||||||
}
|
|
||||||
if tgt.URL != "127.0.0.1:5000" {
|
|
||||||
t.Errorf("Unexpected url in target: %s, expected 127.0.0.1:5000", tgt.URL)
|
|
||||||
}
|
|
||||||
if tgt.Username != "admin" {
|
|
||||||
t.Errorf("Unexpected username in target: %s, expected admin", tgt.Username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepTargetByName(t *testing.T) {
|
|
||||||
target, err := GetRepTarget(targetID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get target %d: %v", targetID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
target2, err := GetRepTargetByName(target.Name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get target %s: %v", target.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.Name != target2.Name {
|
|
||||||
t.Errorf("unexpected target name: %s, expected: %s", target2.Name, target.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepTargetByEndpoint(t *testing.T) {
|
|
||||||
target, err := GetRepTarget(targetID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get target %d: %v", targetID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
target2, err := GetRepTargetByEndpoint(target.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get target %s: %v", target.URL, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.URL != target2.URL {
|
|
||||||
t.Errorf("unexpected target URL: %s, expected: %s", target2.URL, target.URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateRepTarget(t *testing.T) {
|
|
||||||
target := &models.RepTarget{
|
|
||||||
Name: "name",
|
|
||||||
URL: "http://url",
|
|
||||||
Username: "username",
|
|
||||||
Password: "password",
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := AddRepTarget(*target)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to add target: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := DeleteRepTarget(id); err != nil {
|
|
||||||
t.Logf("failed to delete target %d: %v", id, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
target.ID = id
|
|
||||||
target.Name = "new_name"
|
|
||||||
target.URL = "http://new_url"
|
|
||||||
target.Username = "new_username"
|
|
||||||
target.Password = "new_password"
|
|
||||||
|
|
||||||
if err = UpdateRepTarget(*target); err != nil {
|
|
||||||
t.Fatalf("failed to update target: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err = GetRepTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get target %d: %v", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.Name != "new_name" {
|
|
||||||
t.Errorf("unexpected name: %s, expected: %s", target.Name, "new_name")
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.URL != "http://new_url" {
|
|
||||||
t.Errorf("unexpected url: %s, expected: %s", target.URL, "http://new_url")
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.Username != "new_username" {
|
|
||||||
t.Errorf("unexpected username: %s, expected: %s", target.Username, "new_username")
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.Password != "new_password" {
|
|
||||||
t.Errorf("unexpected password: %s, expected: %s", target.Password, "new_password")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterRepTargets(t *testing.T) {
|
|
||||||
targets, err := FilterRepTargets("test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get all targets: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(targets) == 0 {
|
|
||||||
t.Errorf("unexpected num of targets: %d, expected: %d", len(targets), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddRepPolicy(t *testing.T) {
|
|
||||||
policy := models.RepPolicy{
|
|
||||||
ProjectID: 1,
|
|
||||||
TargetID: targetID,
|
|
||||||
Description: "whatever",
|
|
||||||
Name: "mypolicy",
|
|
||||||
}
|
|
||||||
id, err := AddRepPolicy(policy)
|
|
||||||
t.Logf("added policy, id: %d", id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in AddRepPolicy: %v", err)
|
|
||||||
} else {
|
|
||||||
policyID = id
|
|
||||||
}
|
|
||||||
p, err := GetRepPolicy(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetPolicy: %v, id: %d", err, id)
|
|
||||||
}
|
|
||||||
if p == nil {
|
|
||||||
t.Errorf("Unable to find a policy with id: %d", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Name != "mypolicy" || p.TargetID != targetID || p.Description != "whatever" {
|
|
||||||
t.Errorf("The data does not match, expected: Name: mypolicy, TargetID: %d, Description: whatever;\n result: Name: %s, TargetID: %d, Description: %s",
|
|
||||||
targetID, p.Name, p.TargetID, p.Description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepPolicyByTarget(t *testing.T) {
|
|
||||||
policies, err := GetRepPolicyByTarget(targetID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get policy according target %d: %v", targetID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(policies) == 0 {
|
|
||||||
t.Fatal("unexpected length of policies 0, expected is >0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if policies[0].ID != policyID {
|
|
||||||
t.Fatalf("unexpected policy: %d, expected: %d", policies[0].ID, policyID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepPolicyByProjectAndTarget(t *testing.T) {
|
|
||||||
policies, err := GetRepPolicyByProjectAndTarget(1, targetID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get policy according project %d and target %d: %v", 1, targetID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(policies) == 0 {
|
|
||||||
t.Fatal("unexpected length of policies 0, expected is >0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if policies[0].ID != policyID {
|
|
||||||
t.Fatalf("unexpected policy: %d, expected: %d", policies[0].ID, policyID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepPolicyByName(t *testing.T) {
|
|
||||||
policy, err := GetRepPolicy(policyID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get policy %d: %v", policyID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
policy2, err := GetRepPolicyByName(policy.Name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get policy %s: %v", policy.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.Name != policy2.Name {
|
|
||||||
t.Errorf("unexpected name: %s, expected: %s", policy2.Name, policy.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddRepJob(t *testing.T) {
|
|
||||||
job := models.RepJob{
|
|
||||||
Repository: "library/ubuntu",
|
|
||||||
PolicyID: policyID,
|
|
||||||
Operation: "transfer",
|
|
||||||
TagList: []string{"12.01", "14.04", "latest"},
|
|
||||||
}
|
|
||||||
id, err := AddRepJob(job)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in AddRepJob: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jobID = id
|
|
||||||
|
|
||||||
j, err := GetRepJob(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetRepJob: %v, id: %d", err, id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if j == nil {
|
|
||||||
t.Errorf("Unable to find a job with id: %d", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if j.Status != models.JobPending || j.Repository != "library/ubuntu" || j.PolicyID != policyID || j.Operation != "transfer" || len(j.TagList) != 3 {
|
|
||||||
t.Errorf("Expected data of job, id: %d, Status: %s, Repository: library/ubuntu, PolicyID: %d, Operation: transfer, taglist length 3"+
|
|
||||||
"but in returned data:, Status: %s, Repository: %s, Operation: %s, PolicyID: %d, TagList: %v", id, models.JobPending, policyID, j.Status, j.Repository, j.Operation, j.PolicyID, j.TagList)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetRepJobUUID(t *testing.T) {
|
|
||||||
uuid := "u-rep-job-uuid"
|
|
||||||
assert := assert.New(t)
|
|
||||||
err := SetRepJobUUID(jobID, uuid)
|
|
||||||
assert.Nil(err)
|
|
||||||
j, err := GetRepJob(jobID)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(uuid, j.UUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateRepJobStatus(t *testing.T) {
|
|
||||||
err := UpdateRepJobStatus(jobID, models.JobFinished)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in UpdateRepJobStatus, error: %v, id: %d", err, jobID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
j, err := GetRepJob(jobID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetRepJob: %v, id: %d", err, jobID)
|
|
||||||
}
|
|
||||||
if j == nil {
|
|
||||||
t.Errorf("Unable to find a job with id: %d", jobID)
|
|
||||||
}
|
|
||||||
if j.Status != models.JobFinished {
|
|
||||||
t.Errorf("Job's status: %s, expected: %s, id: %d", j.Status, models.JobFinished, jobID)
|
|
||||||
}
|
|
||||||
err = UpdateRepJobStatus(jobID, models.JobPending)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in UpdateRepJobStatus when update it back to status pending, error: %v, id: %d", err, jobID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepPolicyByProject(t *testing.T) {
|
|
||||||
p1, err := GetRepPolicyByProject(99)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetRepPolicyByProject:%v, project ID: %d", err, 99)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(p1) > 0 {
|
|
||||||
t.Errorf("Unexpected length of policy list, expected: 0, in fact: %d, project id: %d", len(p1), 99)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p2, err := GetRepPolicyByProject(1)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occuered in GetRepPolicyByProject:%v, project ID: %d", err, 2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(p2) != 1 {
|
|
||||||
t.Errorf("Unexpected length of policy list, expected: 1, in fact: %d, project id: %d", len(p2), 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if p2[0].ID != policyID {
|
|
||||||
t.Errorf("Unexpecred policy id in result, expected: %d, in fact: %d", policyID, p2[0].ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepJobs(t *testing.T) {
|
|
||||||
var policyID int64 = 10000
|
|
||||||
repository := "repository_for_test_get_rep_jobs"
|
|
||||||
operation := "operation_for_test"
|
|
||||||
status := "status_for_test"
|
|
||||||
now := time.Now().Add(1 * time.Minute)
|
|
||||||
id, err := AddRepJob(models.RepJob{
|
|
||||||
PolicyID: policyID,
|
|
||||||
Repository: repository,
|
|
||||||
Operation: operation,
|
|
||||||
Status: status,
|
|
||||||
CreationTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer DeleteRepJob(id)
|
|
||||||
|
|
||||||
// no query
|
|
||||||
jobs, err := GetRepJobs()
|
|
||||||
require.Nil(t, err)
|
|
||||||
found := false
|
|
||||||
for _, job := range jobs {
|
|
||||||
if job.ID == id {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found)
|
|
||||||
|
|
||||||
// query by policy ID
|
|
||||||
jobs, err = GetRepJobs(&models.RepJobQuery{
|
|
||||||
PolicyID: policyID,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(jobs))
|
|
||||||
assert.Equal(t, id, jobs[0].ID)
|
|
||||||
|
|
||||||
// query by repository
|
|
||||||
jobs, err = GetRepJobs(&models.RepJobQuery{
|
|
||||||
Repository: repository,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(jobs))
|
|
||||||
assert.Equal(t, id, jobs[0].ID)
|
|
||||||
|
|
||||||
// query by operation
|
|
||||||
jobs, err = GetRepJobs(&models.RepJobQuery{
|
|
||||||
Operations: []string{operation},
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(jobs))
|
|
||||||
assert.Equal(t, id, jobs[0].ID)
|
|
||||||
|
|
||||||
// query by status
|
|
||||||
jobs, err = GetRepJobs(&models.RepJobQuery{
|
|
||||||
Statuses: []string{status},
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(jobs))
|
|
||||||
assert.Equal(t, id, jobs[0].ID)
|
|
||||||
|
|
||||||
// query by creation time
|
|
||||||
jobs, err = GetRepJobs(&models.RepJobQuery{
|
|
||||||
StartTime: &now,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(jobs))
|
|
||||||
assert.Equal(t, id, jobs[0].ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteRepJob(t *testing.T) {
|
|
||||||
err := DeleteRepJob(jobID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in DeleteRepJob: %v, id: %d", err, jobID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Logf("deleted rep job, id: %d", jobID)
|
|
||||||
j, err := GetRepJob(jobID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetRepJob:%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if j != nil {
|
|
||||||
t.Errorf("Able to find rep job after deletion, id: %d", jobID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteRepTarget(t *testing.T) {
|
|
||||||
err := DeleteRepTarget(targetID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in DeleteRepTarget: %v, id: %d", err, targetID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Logf("deleted target, id: %d", targetID)
|
|
||||||
tgt, err := GetRepTarget(targetID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, targetID)
|
|
||||||
}
|
|
||||||
if tgt != nil {
|
|
||||||
t.Errorf("Able to find target after deletion, id: %d", targetID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetTotalOfRepPolicies(t *testing.T) {
|
|
||||||
_, err := GetTotalOfRepPolicies("", 1)
|
|
||||||
require.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterRepPolicies(t *testing.T) {
|
|
||||||
_, err := FilterRepPolicies("name", 0, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to filter policy: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateRepPolicy(t *testing.T) {
|
|
||||||
policy := &models.RepPolicy{
|
|
||||||
ID: policyID,
|
|
||||||
Name: "new_policy_name",
|
|
||||||
}
|
|
||||||
if err := UpdateRepPolicy(policy); err != nil {
|
|
||||||
t.Fatalf("failed to update policy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteRepPolicy(t *testing.T) {
|
|
||||||
err := DeleteRepPolicy(policyID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in DeleteRepPolicy: %v, id: %d", err, policyID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Logf("delete rep policy, id: %d", policyID)
|
|
||||||
p, err := GetRepPolicy(policyID)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Nil(t, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetOrmer(t *testing.T) {
|
func TestGetOrmer(t *testing.T) {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
if o == nil {
|
if o == nil {
|
||||||
|
@ -1,424 +0,0 @@
|
|||||||
// 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 dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/astaxie/beego/orm"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddRepTarget ...
|
|
||||||
func AddRepTarget(target models.RepTarget) (int64, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
|
|
||||||
sql := "insert into replication_target (name, url, username, password, insecure, target_type) values (?, ?, ?, ?, ?, ?) RETURNING id"
|
|
||||||
|
|
||||||
var targetID int64
|
|
||||||
err := o.Raw(sql, target.Name, target.URL, target.Username, target.Password, target.Insecure, target.Type).QueryRow(&targetID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return targetID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepTarget ...
|
|
||||||
func GetRepTarget(id int64) (*models.RepTarget, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
t := models.RepTarget{ID: id}
|
|
||||||
err := o.Read(&t)
|
|
||||||
if err == orm.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return &t, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepTargetByName ...
|
|
||||||
func GetRepTargetByName(name string) (*models.RepTarget, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
t := models.RepTarget{Name: name}
|
|
||||||
err := o.Read(&t, "Name")
|
|
||||||
if err == orm.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return &t, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepTargetByEndpoint ...
|
|
||||||
func GetRepTargetByEndpoint(endpoint string) (*models.RepTarget, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
t := models.RepTarget{
|
|
||||||
URL: endpoint,
|
|
||||||
}
|
|
||||||
err := o.Read(&t, "URL")
|
|
||||||
if err == orm.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return &t, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRepTarget ...
|
|
||||||
func DeleteRepTarget(id int64) error {
|
|
||||||
o := GetOrmer()
|
|
||||||
_, err := o.Delete(&models.RepTarget{ID: id})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRepTarget ...
|
|
||||||
func UpdateRepTarget(target models.RepTarget) error {
|
|
||||||
o := GetOrmer()
|
|
||||||
|
|
||||||
sql := `update replication_target
|
|
||||||
set url = ?, name = ?, username = ?, password = ?, insecure = ?, update_time = ?
|
|
||||||
where id = ?`
|
|
||||||
|
|
||||||
_, err := o.Raw(sql, target.URL, target.Name, target.Username, target.Password, target.Insecure, time.Now(), target.ID).Exec()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterRepTargets filters targets by name
|
|
||||||
func FilterRepTargets(name string) ([]*models.RepTarget, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
|
|
||||||
var args []interface{}
|
|
||||||
|
|
||||||
sql := `select * from replication_target `
|
|
||||||
if len(name) != 0 {
|
|
||||||
sql += `where name like ? `
|
|
||||||
args = append(args, "%"+Escape(name)+"%")
|
|
||||||
}
|
|
||||||
sql += `order by creation_time`
|
|
||||||
|
|
||||||
var targets []*models.RepTarget
|
|
||||||
|
|
||||||
if _, err := o.Raw(sql, args).QueryRows(&targets); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRepPolicy ...
|
|
||||||
func AddRepPolicy(policy models.RepPolicy) (int64, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
sql := `insert into replication_policy (name, project_id, target_id, enabled, description, cron_str, creation_time, update_time, filters, replicate_deletion)
|
|
||||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id`
|
|
||||||
params := []interface{}{}
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
params = append(params, policy.Name, policy.ProjectID, policy.TargetID, true,
|
|
||||||
policy.Description, policy.Trigger, now, now, policy.Filters,
|
|
||||||
policy.ReplicateDeletion)
|
|
||||||
|
|
||||||
var policyID int64
|
|
||||||
err := o.Raw(sql, params...).QueryRow(&policyID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return policyID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepPolicy ...
|
|
||||||
func GetRepPolicy(id int64) (*models.RepPolicy, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
sql := `select * from replication_policy where id = ? and deleted = false`
|
|
||||||
|
|
||||||
var policy models.RepPolicy
|
|
||||||
|
|
||||||
if err := o.Raw(sql, id).QueryRow(&policy); err != nil {
|
|
||||||
if err == orm.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &policy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTotalOfRepPolicies returns the total count of replication policies
|
|
||||||
func GetTotalOfRepPolicies(name string, projectID int64) (int64, error) {
|
|
||||||
qs := GetOrmer().QueryTable(&models.RepPolicy{}).Filter("deleted", false)
|
|
||||||
|
|
||||||
if len(name) != 0 {
|
|
||||||
qs = qs.Filter("name__icontains", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if projectID != 0 {
|
|
||||||
qs = qs.Filter("project_id", projectID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return qs.Count()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterRepPolicies filters policies by name and project ID
|
|
||||||
func FilterRepPolicies(name string, projectID, page, pageSize int64) ([]*models.RepPolicy, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
|
|
||||||
var args []interface{}
|
|
||||||
|
|
||||||
sql := `select rp.id, rp.project_id, rp.target_id,
|
|
||||||
rt.name as target_name, rp.name, rp.description,
|
|
||||||
rp.cron_str, rp.filters, rp.replicate_deletion,
|
|
||||||
rp.creation_time, rp.update_time,
|
|
||||||
count(rj.status) as error_job_count
|
|
||||||
from replication_policy rp
|
|
||||||
left join replication_target rt on rp.target_id=rt.id
|
|
||||||
left join replication_job rj on rp.id=rj.policy_id and (rj.status='error'
|
|
||||||
or rj.status='retrying')
|
|
||||||
where rp.deleted = false `
|
|
||||||
|
|
||||||
if len(name) != 0 && projectID != 0 {
|
|
||||||
sql += `and rp.name like ? and rp.project_id = ? `
|
|
||||||
args = append(args, "%"+Escape(name)+"%")
|
|
||||||
args = append(args, projectID)
|
|
||||||
} else if len(name) != 0 {
|
|
||||||
sql += `and rp.name like ? `
|
|
||||||
args = append(args, "%"+Escape(name)+"%")
|
|
||||||
} else if projectID != 0 {
|
|
||||||
sql += `and rp.project_id = ? `
|
|
||||||
args = append(args, projectID)
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += `group by rt.name, rp.id order by rp.creation_time`
|
|
||||||
|
|
||||||
if page > 0 && pageSize > 0 {
|
|
||||||
sql += ` limit ? offset ?`
|
|
||||||
args = append(args, pageSize, (page-1)*pageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
var policies []*models.RepPolicy
|
|
||||||
if _, err := o.Raw(sql, args).QueryRows(&policies); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return policies, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepPolicyByName ...
|
|
||||||
func GetRepPolicyByName(name string) (*models.RepPolicy, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
sql := `select * from replication_policy where deleted = false and name = ?`
|
|
||||||
|
|
||||||
var policy models.RepPolicy
|
|
||||||
|
|
||||||
if err := o.Raw(sql, name).QueryRow(&policy); err != nil {
|
|
||||||
if err == orm.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &policy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepPolicyByProject ...
|
|
||||||
func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
sql := `select * from replication_policy where deleted = false and project_id = ?`
|
|
||||||
|
|
||||||
var policies []*models.RepPolicy
|
|
||||||
|
|
||||||
if _, err := o.Raw(sql, projectID).QueryRows(&policies); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return policies, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepPolicyByTarget ...
|
|
||||||
func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
sql := `select * from replication_policy where deleted = false and target_id = ?`
|
|
||||||
|
|
||||||
var policies []*models.RepPolicy
|
|
||||||
|
|
||||||
if _, err := o.Raw(sql, targetID).QueryRows(&policies); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return policies, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepPolicyByProjectAndTarget ...
|
|
||||||
func GetRepPolicyByProjectAndTarget(projectID, targetID int64) ([]*models.RepPolicy, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
sql := `select * from replication_policy where deleted = false and project_id = ? and target_id = ?`
|
|
||||||
|
|
||||||
var policies []*models.RepPolicy
|
|
||||||
|
|
||||||
if _, err := o.Raw(sql, projectID, targetID).QueryRows(&policies); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return policies, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRepPolicy ...
|
|
||||||
func UpdateRepPolicy(policy *models.RepPolicy) error {
|
|
||||||
o := GetOrmer()
|
|
||||||
|
|
||||||
sql := `update replication_policy
|
|
||||||
set project_id = ?, target_id = ?, name = ?, description = ?, cron_str = ?, filters = ?, replicate_deletion = ?, update_time = ?
|
|
||||||
where id = ?`
|
|
||||||
|
|
||||||
_, err := o.Raw(sql, policy.ProjectID, policy.TargetID, policy.Name, policy.Description, policy.Trigger, policy.Filters, policy.ReplicateDeletion, time.Now(), policy.ID).Exec()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRepPolicy ...
|
|
||||||
func DeleteRepPolicy(id int64) error {
|
|
||||||
_, err := GetOrmer().Delete(&models.RepPolicy{
|
|
||||||
ID: id,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRepJob ...
|
|
||||||
func AddRepJob(job models.RepJob) (int64, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
if len(job.Status) == 0 {
|
|
||||||
job.Status = models.JobPending
|
|
||||||
}
|
|
||||||
if len(job.TagList) > 0 {
|
|
||||||
job.Tags = strings.Join(job.TagList, ",")
|
|
||||||
}
|
|
||||||
return o.Insert(&job)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepJob ...
|
|
||||||
func GetRepJob(id int64) (*models.RepJob, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
j := models.RepJob{ID: id}
|
|
||||||
err := o.Read(&j)
|
|
||||||
if err == orm.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
genTagListForJob(&j)
|
|
||||||
return &j, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTotalCountOfRepJobs ...
|
|
||||||
func GetTotalCountOfRepJobs(query ...*models.RepJobQuery) (int64, error) {
|
|
||||||
qs := repJobQueryConditions(query...)
|
|
||||||
return qs.Count()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepJobs ...
|
|
||||||
func GetRepJobs(query ...*models.RepJobQuery) ([]*models.RepJob, error) {
|
|
||||||
jobs := []*models.RepJob{}
|
|
||||||
|
|
||||||
qs := repJobQueryConditions(query...)
|
|
||||||
if len(query) > 0 && query[0] != nil {
|
|
||||||
qs = paginateForQuerySetter(qs, query[0].Page, query[0].Size)
|
|
||||||
}
|
|
||||||
|
|
||||||
qs = qs.OrderBy("-UpdateTime")
|
|
||||||
|
|
||||||
if _, err := qs.All(&jobs); err != nil {
|
|
||||||
return jobs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
genTagListForJob(jobs...)
|
|
||||||
|
|
||||||
return jobs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func repJobQueryConditions(query ...*models.RepJobQuery) orm.QuerySeter {
|
|
||||||
qs := GetOrmer().QueryTable(new(models.RepJob))
|
|
||||||
if len(query) == 0 || query[0] == nil {
|
|
||||||
return qs
|
|
||||||
}
|
|
||||||
|
|
||||||
q := query[0]
|
|
||||||
if q.PolicyID != 0 {
|
|
||||||
qs = qs.Filter("PolicyID", q.PolicyID)
|
|
||||||
}
|
|
||||||
if len(q.OpUUID) > 0 {
|
|
||||||
qs = qs.Filter("OpUUID__exact", q.OpUUID)
|
|
||||||
}
|
|
||||||
if len(q.Repository) > 0 {
|
|
||||||
qs = qs.Filter("Repository__icontains", q.Repository)
|
|
||||||
}
|
|
||||||
if len(q.Statuses) > 0 {
|
|
||||||
qs = qs.Filter("Status__in", q.Statuses)
|
|
||||||
}
|
|
||||||
if len(q.Operations) > 0 {
|
|
||||||
qs = qs.Filter("Operation__in", q.Operations)
|
|
||||||
}
|
|
||||||
if q.StartTime != nil {
|
|
||||||
qs = qs.Filter("CreationTime__gte", q.StartTime)
|
|
||||||
}
|
|
||||||
if q.EndTime != nil {
|
|
||||||
qs = qs.Filter("CreationTime__lte", q.EndTime)
|
|
||||||
}
|
|
||||||
return qs
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRepJob ...
|
|
||||||
func DeleteRepJob(id int64) error {
|
|
||||||
o := GetOrmer()
|
|
||||||
_, err := o.Delete(&models.RepJob{ID: id})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRepJobs deletes replication jobs by policy ID
|
|
||||||
func DeleteRepJobs(policyID int64) error {
|
|
||||||
_, err := GetOrmer().QueryTable(&models.RepJob{}).Filter("PolicyID", policyID).Delete()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateRepJobStatus ...
|
|
||||||
func UpdateRepJobStatus(id int64, status string) error {
|
|
||||||
o := GetOrmer()
|
|
||||||
j := models.RepJob{
|
|
||||||
ID: id,
|
|
||||||
Status: status,
|
|
||||||
UpdateTime: time.Now(),
|
|
||||||
}
|
|
||||||
n, err := o.Update(&j, "Status", "UpdateTime")
|
|
||||||
if n == 0 {
|
|
||||||
log.Warningf("no records are updated when updating replication job %d", id)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRepJobUUID ...
|
|
||||||
func SetRepJobUUID(id int64, uuid string) error {
|
|
||||||
o := GetOrmer()
|
|
||||||
j := models.RepJob{
|
|
||||||
ID: id,
|
|
||||||
UUID: uuid,
|
|
||||||
}
|
|
||||||
n, err := o.Update(&j, "UUID")
|
|
||||||
if n == 0 {
|
|
||||||
log.Warningf("no records are updated when updating replication job %d", id)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func genTagListForJob(jobs ...*models.RepJob) {
|
|
||||||
for _, j := range jobs {
|
|
||||||
if len(j.Tags) > 0 {
|
|
||||||
j.TagList = strings.Split(j.Tags, ",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
// 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 dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDeleteRepJobs(t *testing.T) {
|
|
||||||
var policyID int64 = 999
|
|
||||||
_, err := AddRepJob(models.RepJob{
|
|
||||||
PolicyID: policyID,
|
|
||||||
Repository: "library/hello-world",
|
|
||||||
Operation: "delete",
|
|
||||||
Status: "success",
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
_, err = AddRepJob(models.RepJob{
|
|
||||||
PolicyID: policyID,
|
|
||||||
Repository: "library/hello-world",
|
|
||||||
Operation: "delete",
|
|
||||||
Status: "success",
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
jobs, err := GetRepJobs(&models.RepJobQuery{
|
|
||||||
PolicyID: policyID,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 2, len(jobs))
|
|
||||||
|
|
||||||
err = DeleteRepJobs(policyID)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
jobs, err = GetRepJobs(&models.RepJobQuery{
|
|
||||||
PolicyID: policyID,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, 0, len(jobs))
|
|
||||||
}
|
|
@ -18,7 +18,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/astaxie/beego/orm"
|
"github.com/astaxie/beego/orm"
|
||||||
_ "github.com/mattn/go-sqlite3" // register sqlite driver
|
// _ "github.com/mattn/go-sqlite3" // register sqlite driver
|
||||||
)
|
)
|
||||||
|
|
||||||
type sqlite struct {
|
type sqlite struct {
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
// 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 dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultDatabaseWatchItemDAO is an instance of DatabaseWatchItemDAO
|
|
||||||
var DefaultDatabaseWatchItemDAO WatchItemDAO = &DatabaseWatchItemDAO{}
|
|
||||||
|
|
||||||
// WatchItemDAO defines operations about WatchItem
|
|
||||||
type WatchItemDAO interface {
|
|
||||||
Add(*models.WatchItem) (int64, error)
|
|
||||||
DeleteByPolicyID(int64) error
|
|
||||||
Get(namespace, operation string) ([]models.WatchItem, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DatabaseWatchItemDAO implements interface WatchItemDAO for database
|
|
||||||
type DatabaseWatchItemDAO struct{}
|
|
||||||
|
|
||||||
// Add a WatchItem
|
|
||||||
func (d *DatabaseWatchItemDAO) Add(item *models.WatchItem) (int64, error) {
|
|
||||||
o := GetOrmer()
|
|
||||||
|
|
||||||
var triggerID int64
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
sql := "insert into replication_immediate_trigger (policy_id, namespace, on_deletion, on_push, creation_time, update_time) values (?, ?, ?, ?, ?, ?) RETURNING id"
|
|
||||||
|
|
||||||
err := o.Raw(sql, item.PolicyID, item.Namespace, item.OnDeletion, item.OnPush, now, now).QueryRow(&triggerID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return triggerID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteByPolicyID deletes the WatchItem specified by policy ID
|
|
||||||
func (d *DatabaseWatchItemDAO) DeleteByPolicyID(policyID int64) error {
|
|
||||||
_, err := GetOrmer().QueryTable(&models.WatchItem{}).Filter("PolicyID", policyID).Delete()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns WatchItem list according to the namespace and operation
|
|
||||||
func (d *DatabaseWatchItemDAO) Get(namespace, operation string) ([]models.WatchItem, error) {
|
|
||||||
qs := GetOrmer().QueryTable(&models.WatchItem{}).Filter("Namespace", namespace)
|
|
||||||
if operation == "push" {
|
|
||||||
qs = qs.Filter("OnPush", true)
|
|
||||||
} else if operation == "delete" {
|
|
||||||
qs = qs.Filter("OnDeletion", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
items := []models.WatchItem{}
|
|
||||||
_, err := qs.All(&items)
|
|
||||||
return items, err
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
// 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 dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMethodsOfWatchItem(t *testing.T) {
|
|
||||||
targetID, err := AddRepTarget(models.RepTarget{
|
|
||||||
Name: "test_target_for_watch_item",
|
|
||||||
URL: "http://127.0.0.1",
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer DeleteRepTarget(targetID)
|
|
||||||
|
|
||||||
policyID, err := AddRepPolicy(models.RepPolicy{
|
|
||||||
Name: "test_policy_for_watch_item",
|
|
||||||
ProjectID: 1,
|
|
||||||
TargetID: targetID,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer DeleteRepPolicy(policyID)
|
|
||||||
|
|
||||||
item := &models.WatchItem{
|
|
||||||
PolicyID: policyID,
|
|
||||||
Namespace: "library",
|
|
||||||
OnPush: false,
|
|
||||||
OnDeletion: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// test Add
|
|
||||||
id, err := DefaultDatabaseWatchItemDAO.Add(item)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
// test Get: operation-push
|
|
||||||
items, err := DefaultDatabaseWatchItemDAO.Get("library", "push")
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, 0, len(items))
|
|
||||||
|
|
||||||
// test Get: operation-delete
|
|
||||||
items, err = DefaultDatabaseWatchItemDAO.Get("library", "delete")
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, 1, len(items))
|
|
||||||
assert.Equal(t, id, items[0].ID)
|
|
||||||
assert.Equal(t, "library", items[0].Namespace)
|
|
||||||
assert.True(t, items[0].OnDeletion)
|
|
||||||
|
|
||||||
// test DeleteByPolicyID
|
|
||||||
err = DefaultDatabaseWatchItemDAO.DeleteByPolicyID(policyID)
|
|
||||||
require.Nil(t, err)
|
|
||||||
items, err = DefaultDatabaseWatchItemDAO.Get("library", "delete")
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, 0, len(items))
|
|
||||||
}
|
|
@ -95,6 +95,9 @@ func (c *Client) Head(url string) error {
|
|||||||
func (c *Client) Post(url string, v ...interface{}) error {
|
func (c *Client) Post(url string, v ...interface{}) error {
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
|
if r, ok := v[0].(io.Reader); ok {
|
||||||
|
reader = r
|
||||||
|
} else {
|
||||||
data, err := json.Marshal(v[0])
|
data, err := json.Marshal(v[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -102,6 +105,7 @@ func (c *Client) Post(url string, v ...interface{}) error {
|
|||||||
|
|
||||||
reader = bytes.NewReader(data)
|
reader = bytes.NewReader(data)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, url, reader)
|
req, err := http.NewRequest(http.MethodPost, url, reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -5,14 +5,12 @@ const (
|
|||||||
ImageScanJob = "IMAGE_SCAN"
|
ImageScanJob = "IMAGE_SCAN"
|
||||||
// ImageScanAllJob is the name of "scanall" job in job service
|
// ImageScanAllJob is the name of "scanall" job in job service
|
||||||
ImageScanAllJob = "IMAGE_SCAN_ALL"
|
ImageScanAllJob = "IMAGE_SCAN_ALL"
|
||||||
// ImageTransfer : the name of image transfer job in job service
|
|
||||||
ImageTransfer = "IMAGE_TRANSFER"
|
|
||||||
// ImageDelete : the name of image delete job in job service
|
|
||||||
ImageDelete = "IMAGE_DELETE"
|
|
||||||
// ImageReplicate : the name of image replicate job in job service
|
|
||||||
ImageReplicate = "IMAGE_REPLICATE"
|
|
||||||
// ImageGC the name of image garbage collection job in job service
|
// ImageGC the name of image garbage collection job in job service
|
||||||
ImageGC = "IMAGE_GC"
|
ImageGC = "IMAGE_GC"
|
||||||
|
// Replication : the name of the replication job in job service
|
||||||
|
Replication = "REPLICATION"
|
||||||
|
// ReplicationScheduler : the name of the replication scheduler job in job service
|
||||||
|
ReplicationScheduler = "IMAGE_REPLICATE"
|
||||||
|
|
||||||
// JobKindGeneric : Kind of generic job
|
// JobKindGeneric : Kind of generic job
|
||||||
JobKindGeneric = "Generic"
|
JobKindGeneric = "Generic"
|
||||||
|
@ -27,7 +27,7 @@ func currPath() string {
|
|||||||
return path.Dir(f)
|
return path.Dir(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJobServiceServer
|
// NewJobServiceServer ...
|
||||||
func NewJobServiceServer() *httptest.Server {
|
func NewJobServiceServer() *httptest.Server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc(fmt.Sprintf("%s/%s/log", jobsPrefix, jobUUID),
|
mux.HandleFunc(fmt.Sprintf("%s/%s/log", jobsPrefix, jobUUID),
|
||||||
|
@ -19,9 +19,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
orm.RegisterModel(new(RepTarget),
|
orm.RegisterModel(
|
||||||
new(RepPolicy),
|
|
||||||
new(RepJob),
|
|
||||||
new(User),
|
new(User),
|
||||||
new(Project),
|
new(Project),
|
||||||
new(Role),
|
new(Role),
|
||||||
@ -30,7 +28,6 @@ func init() {
|
|||||||
new(RepoRecord),
|
new(RepoRecord),
|
||||||
new(ImgScanOverview),
|
new(ImgScanOverview),
|
||||||
new(ClairVulnTimestamp),
|
new(ClairVulnTimestamp),
|
||||||
new(WatchItem),
|
|
||||||
new(ProjectMetadata),
|
new(ProjectMetadata),
|
||||||
new(ConfigEntry),
|
new(ConfigEntry),
|
||||||
new(Label),
|
new(Label),
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
// 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 models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/astaxie/beego/validation"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// RepOpTransfer represents the operation of a job to transfer repository to a remote registry/harbor instance.
|
|
||||||
RepOpTransfer string = "transfer"
|
|
||||||
// RepOpDelete represents the operation of a job to remove repository from a remote registry/harbor instance.
|
|
||||||
RepOpDelete string = "delete"
|
|
||||||
// RepOpSchedule represents the operation of a job to schedule the real replication process
|
|
||||||
RepOpSchedule string = "schedule"
|
|
||||||
// RepTargetTable is the table name for replication targets
|
|
||||||
RepTargetTable = "replication_target"
|
|
||||||
// RepJobTable is the table name for replication jobs
|
|
||||||
RepJobTable = "replication_job"
|
|
||||||
// RepPolicyTable is table name for replication policies
|
|
||||||
RepPolicyTable = "replication_policy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RepPolicy is the model for a replication policy, which associate to a project and a target (destination)
|
|
||||||
type RepPolicy struct {
|
|
||||||
ID int64 `orm:"pk;auto;column(id)"`
|
|
||||||
ProjectID int64 `orm:"column(project_id)" `
|
|
||||||
TargetID int64 `orm:"column(target_id)"`
|
|
||||||
Name string `orm:"column(name)"`
|
|
||||||
Description string `orm:"column(description)"`
|
|
||||||
Trigger string `orm:"column(cron_str)"`
|
|
||||||
Filters string `orm:"column(filters)"`
|
|
||||||
ReplicateDeletion bool `orm:"column(replicate_deletion)"`
|
|
||||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add"`
|
|
||||||
UpdateTime time.Time `orm:"column(update_time);auto_now"`
|
|
||||||
Deleted bool `orm:"column(deleted)"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepJob is the model for a replication job, which is the execution unit on job service, currently it is used to transfer/remove
|
|
||||||
// a repository to/from a remote registry instance.
|
|
||||||
type RepJob struct {
|
|
||||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
|
||||||
Status string `orm:"column(status)" json:"status"`
|
|
||||||
Repository string `orm:"column(repository)" json:"repository"`
|
|
||||||
PolicyID int64 `orm:"column(policy_id)" json:"policy_id"`
|
|
||||||
OpUUID string `orm:"column(op_uuid)" json:"op_uuid"`
|
|
||||||
Operation string `orm:"column(operation)" json:"operation"`
|
|
||||||
Tags string `orm:"column(tags)" json:"-"`
|
|
||||||
TagList []string `orm:"-" json:"tags"`
|
|
||||||
UUID string `orm:"column(job_uuid)" json:"-"`
|
|
||||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
|
||||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepTarget is the model for a replication targe, i.e. destination, which wraps the endpoint URL and username/password of a remote registry.
|
|
||||||
type RepTarget struct {
|
|
||||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
|
||||||
URL string `orm:"column(url)" json:"endpoint"`
|
|
||||||
Name string `orm:"column(name)" json:"name"`
|
|
||||||
Username string `orm:"column(username)" json:"username"`
|
|
||||||
Password string `orm:"column(password)" json:"password"`
|
|
||||||
Type int `orm:"column(target_type)" json:"type"`
|
|
||||||
Insecure bool `orm:"column(insecure)" json:"insecure"`
|
|
||||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
|
||||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid ...
|
|
||||||
func (r *RepTarget) Valid(v *validation.Validation) {
|
|
||||||
if len(r.Name) == 0 {
|
|
||||||
v.SetError("name", "can not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Name) > 64 {
|
|
||||||
v.SetError("name", "max length is 64")
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := utils.ParseEndpoint(r.URL)
|
|
||||||
if err != nil {
|
|
||||||
v.SetError("endpoint", err.Error())
|
|
||||||
} else {
|
|
||||||
// Prevent SSRF security issue #3755
|
|
||||||
r.URL = url.Scheme + "://" + url.Host + url.Path
|
|
||||||
if len(r.URL) > 64 {
|
|
||||||
v.SetError("endpoint", "max length is 64")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// password is encoded using base64, the length of this field
|
|
||||||
// in DB is 64, so the max length in request is 48
|
|
||||||
if len(r.Password) > 48 {
|
|
||||||
v.SetError("password", "max length is 48")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName is required by by beego orm to map RepTarget to table replication_target
|
|
||||||
func (r *RepTarget) TableName() string {
|
|
||||||
return RepTargetTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName is required by by beego orm to map RepJob to table replication_job
|
|
||||||
func (r *RepJob) TableName() string {
|
|
||||||
return RepJobTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName is required by by beego orm to map RepPolicy to table replication_policy
|
|
||||||
func (r *RepPolicy) TableName() string {
|
|
||||||
return RepPolicyTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepJobQuery holds query conditions for replication job
|
|
||||||
type RepJobQuery struct {
|
|
||||||
PolicyID int64
|
|
||||||
OpUUID string
|
|
||||||
Repository string
|
|
||||||
Statuses []string
|
|
||||||
Operations []string
|
|
||||||
StartTime *time.Time
|
|
||||||
EndTime *time.Time
|
|
||||||
Pagination
|
|
||||||
}
|
|
@ -42,8 +42,10 @@ const (
|
|||||||
ResourceLog = Resource("log")
|
ResourceLog = Resource("log")
|
||||||
ResourceMember = Resource("member")
|
ResourceMember = Resource("member")
|
||||||
ResourceMetadata = Resource("metadata")
|
ResourceMetadata = Resource("metadata")
|
||||||
ResourceReplication = Resource("replication")
|
ResourceReplication = Resource("replication") // TODO remove
|
||||||
ResourceReplicationJob = Resource("replication-job")
|
ResourceReplicationJob = Resource("replication-job") // TODO remove
|
||||||
|
ResourceReplicationExecution = Resource("replication-execution")
|
||||||
|
ResourceReplicationTask = Resource("replication-task")
|
||||||
ResourceRepository = Resource("repository")
|
ResourceRepository = Resource("repository")
|
||||||
ResourceRepositoryLabel = Resource("repository-label")
|
ResourceRepositoryLabel = Resource("repository-label")
|
||||||
ResourceRepositoryTag = Resource("repository-tag")
|
ResourceRepositoryTag = Resource("repository-tag")
|
||||||
|
@ -75,6 +75,18 @@ var (
|
|||||||
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
|
||||||
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList},
|
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList},
|
||||||
|
|
||||||
|
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionRead},
|
||||||
|
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionList},
|
||||||
|
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionCreate},
|
||||||
|
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionUpdate},
|
||||||
|
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionDelete},
|
||||||
|
|
||||||
|
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionRead},
|
||||||
|
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionList},
|
||||||
|
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionCreate},
|
||||||
|
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionUpdate},
|
||||||
|
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionDelete},
|
||||||
|
|
||||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
|
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
|
||||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
||||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
|
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
|
||||||
|
@ -38,7 +38,12 @@ func NewBasicAuthCredential(username, password string) Credential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *basicAuthCredential) AddAuthorization(req *http.Request) {
|
func (b *basicAuthCredential) AddAuthorization(req *http.Request) {
|
||||||
|
// only add the authentication info when the username isn't empty
|
||||||
|
// the logic is needed for requesting resources from docker hub's
|
||||||
|
// public repositories
|
||||||
|
if len(b.username) > 0 {
|
||||||
req.SetBasicAuth(b.username, b.password)
|
req.SetBasicAuth(b.username, b.password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// implement github.com/goharbor/harbor/src/common/http/modifier.Modifier
|
// implement github.com/goharbor/harbor/src/common/http/modifier.Modifier
|
||||||
|
@ -278,7 +278,7 @@ func NewStandardTokenAuthorizer(client *http.Client, credential Credential,
|
|||||||
// 1. performance issue
|
// 1. performance issue
|
||||||
// 2. the realm field returned by registry is an IP which can not reachable
|
// 2. the realm field returned by registry is an IP which can not reachable
|
||||||
// inside Harbor
|
// inside Harbor
|
||||||
if len(customizedTokenService) > 0 {
|
if len(customizedTokenService) > 0 && len(customizedTokenService[0]) > 0 {
|
||||||
generator.realm = customizedTokenService[0]
|
generator.realm = customizedTokenService[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,3 +157,21 @@ func (r *Registry) Ping() error {
|
|||||||
Message: string(b),
|
Message: string(b),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PingSimple checks whether the registry is available. It checks the connectivity and certificate (if TLS enabled)
|
||||||
|
// only, regardless of credential.
|
||||||
|
func (r *Registry) PingSimple() error {
|
||||||
|
err := r.Ping()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
httpErr, ok := err.(*commonhttp.Error)
|
||||||
|
if !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if httpErr.Code == http.StatusUnauthorized ||
|
||||||
|
httpErr.Code == http.StatusForbidden {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return httpErr
|
||||||
|
}
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
// 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 test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/goharbor/harbor/src/replication"
|
|
||||||
"github.com/goharbor/harbor/src/replication/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakePolicyManager struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FakePolicyManager) GetPolicies(query models.QueryParameter) (*models.ReplicationPolicyQueryResult, error) {
|
|
||||||
return &models.ReplicationPolicyQueryResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FakePolicyManager) GetPolicy(id int64) (models.ReplicationPolicy, error) {
|
|
||||||
return models.ReplicationPolicy{
|
|
||||||
ID: 1,
|
|
||||||
Trigger: &models.Trigger{
|
|
||||||
Kind: replication.TriggerKindManual,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
func (f *FakePolicyManager) CreatePolicy(policy models.ReplicationPolicy) (int64, error) {
|
|
||||||
return 1, nil
|
|
||||||
}
|
|
||||||
func (f *FakePolicyManager) UpdatePolicy(models.ReplicationPolicy) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (f *FakePolicyManager) RemovePolicy(int64) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
// 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 test
|
|
||||||
|
|
||||||
type FakeReplicatoinController struct {
|
|
||||||
FakePolicyManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FakeReplicatoinController) Init() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (f *FakeReplicatoinController) Replicate(policyID int64, metadata ...map[string]interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
// 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 test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FakeWatchItemDAO is the fake implement for the dao.WatchItemDAO
|
|
||||||
type FakeWatchItemDAO struct {
|
|
||||||
items []models.WatchItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ...
|
|
||||||
func (f *FakeWatchItemDAO) Add(item *models.WatchItem) (int64, error) {
|
|
||||||
f.items = append(f.items, *item)
|
|
||||||
return int64(len(f.items) + 1), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteByPolicyID : delete the WatchItem specified by policy ID
|
|
||||||
func (f *FakeWatchItemDAO) DeleteByPolicyID(policyID int64) error {
|
|
||||||
for i, item := range f.items {
|
|
||||||
if item.PolicyID == policyID {
|
|
||||||
f.items = append(f.items[:i], f.items[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns WatchItem list according to the namespace and operation
|
|
||||||
func (f *FakeWatchItemDAO) Get(namespace, operation string) ([]models.WatchItem, error) {
|
|
||||||
items := []models.WatchItem{}
|
|
||||||
for _, item := range f.items {
|
|
||||||
if item.Namespace != namespace {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if operation == "push" {
|
|
||||||
if item.OnPush {
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if operation == "delete" {
|
|
||||||
if item.OnDeletion {
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items, nil
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -297,8 +298,28 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set namespace/repository/version for replication event.
|
||||||
|
_, header, err := cra.GetFile(formFieldNameForChart)
|
||||||
|
if err != nil {
|
||||||
|
cra.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := cra.Ctx.Request
|
||||||
|
charFileName := header.Filename
|
||||||
|
if !strings.HasSuffix(charFileName, ".tgz") {
|
||||||
|
cra.SendInternalServerError(fmt.Errorf("chart file expected %s to end with .tgz", charFileName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
charFileName = strings.TrimSuffix(charFileName, ".tgz")
|
||||||
|
// colon cannot be used as namespace
|
||||||
|
charFileName = strings.Replace(charFileName, "-", ":", -1)
|
||||||
|
// value sample: library:redis:4.0.3 (namespace:repository:version)
|
||||||
|
ctx := context.WithValue(cra.Ctx.Request.Context(), common.ChartUploadCtxKey, cra.namespace+":"+charFileName)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
// Directly proxy to the backend
|
// Directly proxy to the backend
|
||||||
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadChartProvFile handles POST /api/:repo/prov
|
// UploadChartProvFile handles POST /api/:repo/prov
|
||||||
|
@ -19,6 +19,8 @@ import (
|
|||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
rep_dao "github.com/goharbor/harbor/src/replication/dao"
|
||||||
|
rep_models "github.com/goharbor/harbor/src/replication/dao/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -27,10 +29,8 @@ const (
|
|||||||
TestUserPwd = "testUser0001"
|
TestUserPwd = "testUser0001"
|
||||||
TestUserEmail = "testUser0001@mydomain.com"
|
TestUserEmail = "testUser0001@mydomain.com"
|
||||||
TestProName = "testProject0001"
|
TestProName = "testProject0001"
|
||||||
TestTargetName = "testTarget0001"
|
TestRegistryName = "testRegistry0001"
|
||||||
TestRepoName = "testRepo0001"
|
TestRepoName = "testRepo0001"
|
||||||
AdminName = "admin"
|
|
||||||
DefaultProjectName = "library"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func CommonAddUser() {
|
func CommonAddUser() {
|
||||||
@ -83,25 +83,25 @@ func CommonDelProject() {
|
|||||||
_ = dao.DeleteProject(commonProject.ProjectID)
|
_ = dao.DeleteProject(commonProject.ProjectID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CommonAddTarget() {
|
func CommonAddRegistry() {
|
||||||
endPoint := os.Getenv("REGISTRY_URL")
|
endPoint := os.Getenv("REGISTRY_URL")
|
||||||
commonTarget := &models.RepTarget{
|
commonRegistry := &rep_models.Registry{
|
||||||
URL: endPoint,
|
URL: endPoint,
|
||||||
Name: TestTargetName,
|
Name: TestRegistryName,
|
||||||
Username: adminName,
|
AccessKey: adminName,
|
||||||
Password: adminPwd,
|
AccessSecret: adminPwd,
|
||||||
}
|
}
|
||||||
_, _ = dao.AddRepTarget(*commonTarget)
|
_, _ = rep_dao.AddRegistry(commonRegistry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CommonGetTarget() int {
|
func CommonGetRegistry() int {
|
||||||
target, _ := dao.GetRepTargetByName(TestTargetName)
|
registry, _ := rep_dao.GetRegistryByName(TestRegistryName)
|
||||||
return int(target.ID)
|
return int(registry.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CommonDelTarget() {
|
func CommonDelRegistry() {
|
||||||
target, _ := dao.GetRepTargetByName(TestTargetName)
|
registry, _ := rep_dao.GetRegistryByName(TestRegistryName)
|
||||||
_ = dao.DeleteRepTarget(target.ID)
|
_ = rep_dao.DeleteRegistry(registry.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CommonAddRepository() {
|
func CommonAddRepository() {
|
||||||
|
@ -27,25 +27,20 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego"
|
||||||
|
"github.com/dghubble/sling"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/job/test"
|
"github.com/goharbor/harbor/src/common/job/test"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
||||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
api_models "github.com/goharbor/harbor/src/core/api/models"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
apimodels "github.com/goharbor/harbor/src/core/api/models"
|
||||||
"github.com/goharbor/harbor/src/core/filter"
|
|
||||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
|
||||||
|
|
||||||
// "strconv"
|
|
||||||
// "strings"
|
|
||||||
|
|
||||||
"github.com/astaxie/beego"
|
|
||||||
"github.com/dghubble/sling"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
_ "github.com/goharbor/harbor/src/core/auth/db"
|
_ "github.com/goharbor/harbor/src/core/auth/db"
|
||||||
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
||||||
"github.com/goharbor/harbor/src/replication/core"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
_ "github.com/goharbor/harbor/src/replication/event"
|
"github.com/goharbor/harbor/src/core/filter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/tests/apitests/apilib"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -128,14 +123,9 @@ func init() {
|
|||||||
beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests")
|
beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests")
|
||||||
beego.Router("/api/repositories/*/signatures", &RepositoryAPI{}, "get:GetSignatures")
|
beego.Router("/api/repositories/*/signatures", &RepositoryAPI{}, "get:GetSignatures")
|
||||||
beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos")
|
beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos")
|
||||||
beego.Router("/api/targets/", &TargetAPI{}, "get:List")
|
beego.Router("/api/registries", &RegistryAPI{}, "get:List;post:Post")
|
||||||
beego.Router("/api/targets/", &TargetAPI{}, "post:Post")
|
beego.Router("/api/registries/ping", &RegistryAPI{}, "post:Ping")
|
||||||
beego.Router("/api/targets/:id([0-9]+)", &TargetAPI{})
|
beego.Router("/api/registries/:id([0-9]+)", &RegistryAPI{}, "get:Get;put:Put;delete:Delete")
|
||||||
beego.Router("/api/targets/:id([0-9]+)/policies/", &TargetAPI{}, "get:ListPolicies")
|
|
||||||
beego.Router("/api/targets/ping", &TargetAPI{}, "post:Ping")
|
|
||||||
beego.Router("/api/policies/replication/:id([0-9]+)", &RepPolicyAPI{})
|
|
||||||
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "get:List")
|
|
||||||
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "post:Post;delete:Delete")
|
|
||||||
beego.Router("/api/systeminfo", &SystemInfoAPI{}, "get:GetGeneralInfo")
|
beego.Router("/api/systeminfo", &SystemInfoAPI{}, "get:GetGeneralInfo")
|
||||||
beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo")
|
beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo")
|
||||||
beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert")
|
beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert")
|
||||||
@ -146,7 +136,6 @@ func init() {
|
|||||||
beego.Router("/api/configurations", &ConfigAPI{})
|
beego.Router("/api/configurations", &ConfigAPI{})
|
||||||
beego.Router("/api/configs", &ConfigAPI{}, "get:GetInternalConfig")
|
beego.Router("/api/configs", &ConfigAPI{}, "get:GetInternalConfig")
|
||||||
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
||||||
beego.Router("/api/replications", &ReplicationAPI{})
|
|
||||||
beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List")
|
beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List")
|
||||||
beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete")
|
beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete")
|
||||||
beego.Router("/api/labels/:id([0-9]+)/resources", &LabelAPI{}, "get:ListResources")
|
beego.Router("/api/labels/:id([0-9]+)/resources", &LabelAPI{}, "get:ListResources")
|
||||||
@ -159,6 +148,15 @@ func init() {
|
|||||||
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
|
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
|
||||||
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
||||||
|
|
||||||
|
beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List")
|
||||||
|
beego.Router("/api/replication/executions", &ReplicationOperationAPI{}, "get:ListExecutions;post:CreateExecution")
|
||||||
|
beego.Router("/api/replication/executions/:id([0-9]+)", &ReplicationOperationAPI{}, "get:GetExecution;put:StopExecution")
|
||||||
|
beego.Router("/api/replication/executions/:id([0-9]+)/tasks", &ReplicationOperationAPI{}, "get:ListTasks")
|
||||||
|
beego.Router("/api/replication/executions/:id([0-9]+)/tasks/:tid([0-9]+)/log", &ReplicationOperationAPI{}, "get:GetTaskLog")
|
||||||
|
|
||||||
|
beego.Router("/api/replication/policies", &ReplicationPolicyAPI{}, "get:List;post:Create")
|
||||||
|
beego.Router("/api/replication/policies/:id([0-9]+)", &ReplicationPolicyAPI{}, "get:Get;put:Update;delete:Delete")
|
||||||
|
|
||||||
// Charts are controlled under projects
|
// Charts are controlled under projects
|
||||||
chartRepositoryAPIType := &ChartRepositoryAPI{}
|
chartRepositoryAPIType := &ChartRepositoryAPI{}
|
||||||
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
||||||
@ -180,10 +178,6 @@ func init() {
|
|||||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
|
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
|
||||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
|
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
|
||||||
|
|
||||||
if err := core.Init(); err != nil {
|
|
||||||
log.Fatalf("failed to initialize GlobalController: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncRegistry
|
// syncRegistry
|
||||||
if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
|
if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
|
||||||
log.Fatalf("failed to sync repositories from registry: %v", err)
|
log.Fatalf("failed to sync repositories from registry: %v", err)
|
||||||
@ -659,103 +653,6 @@ func (a testapi) GetReposTop(authInfo usrInfo, count string) (int, interface{},
|
|||||||
return http.StatusOK, result, nil
|
return http.StatusOK, result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------Targets Test---------------------------------------//
|
|
||||||
// Create a new replication target
|
|
||||||
func (a testapi) AddTargets(authInfo usrInfo, repTarget apilib.RepTargetPost) (int, string, error) {
|
|
||||||
_sling := sling.New().Post(a.basePath)
|
|
||||||
|
|
||||||
path := "/api/targets"
|
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
_sling = _sling.BodyJSON(repTarget)
|
|
||||||
|
|
||||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
|
||||||
return httpStatusCode, string(body), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// List filters targets by name
|
|
||||||
func (a testapi) ListTargets(authInfo usrInfo, targetName string) (int, []apilib.RepTarget, error) {
|
|
||||||
_sling := sling.New().Get(a.basePath)
|
|
||||||
|
|
||||||
path := "/api/targets?name=" + targetName
|
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
|
|
||||||
var successPayload []apilib.RepTarget
|
|
||||||
|
|
||||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
|
||||||
if err == nil && httpStatusCode == 200 {
|
|
||||||
err = json.Unmarshal(body, &successPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
return httpStatusCode, successPayload, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping target
|
|
||||||
func (a testapi) PingTarget(authInfo usrInfo, body interface{}) (int, error) {
|
|
||||||
_sling := sling.New().Post(a.basePath)
|
|
||||||
|
|
||||||
path := "/api/targets/ping"
|
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
_sling = _sling.BodyJSON(body)
|
|
||||||
|
|
||||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
|
||||||
return httpStatusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get target by targetID
|
|
||||||
func (a testapi) GetTargetByID(authInfo usrInfo, targetID string) (int, error) {
|
|
||||||
_sling := sling.New().Get(a.basePath)
|
|
||||||
|
|
||||||
path := "/api/targets/" + targetID
|
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
|
|
||||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
|
||||||
|
|
||||||
return httpStatusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update target by targetID
|
|
||||||
func (a testapi) PutTargetByID(authInfo usrInfo, targetID string, repTarget apilib.RepTargetPost) (int, error) {
|
|
||||||
_sling := sling.New().Put(a.basePath)
|
|
||||||
|
|
||||||
path := "/api/targets/" + targetID
|
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
_sling = _sling.BodyJSON(repTarget)
|
|
||||||
|
|
||||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
|
||||||
|
|
||||||
return httpStatusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// List the target relevant policies by targetID
|
|
||||||
func (a testapi) GetTargetPoliciesByID(authInfo usrInfo, targetID string) (int, error) {
|
|
||||||
_sling := sling.New().Get(a.basePath)
|
|
||||||
|
|
||||||
path := "/api/targets/" + targetID + "/policies/"
|
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
|
|
||||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
|
||||||
|
|
||||||
return httpStatusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete target by targetID
|
|
||||||
func (a testapi) DeleteTargetsByID(authInfo usrInfo, targetID string) (int, error) {
|
|
||||||
_sling := sling.New().Delete(a.basePath)
|
|
||||||
|
|
||||||
path := "/api/targets/" + targetID
|
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
|
|
||||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
|
||||||
return httpStatusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------Replication_Policy Test--------------------------------//
|
// --------------------Replication_Policy Test--------------------------------//
|
||||||
|
|
||||||
// Create a new replication policy
|
// Create a new replication policy
|
||||||
@ -1244,3 +1141,73 @@ func (a testapi) ScanAllScheduleGet(authInfo usrInfo) (int, api_models.AdminJobS
|
|||||||
|
|
||||||
return httpStatusCode, successPayLoad, err
|
return httpStatusCode, successPayLoad, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a testapi) RegistryGet(authInfo usrInfo, registryID int64) (*model.Registry, int, error) {
|
||||||
|
_sling := sling.New().Base(a.basePath).Get(fmt.Sprintf("/api/registries/%d", registryID))
|
||||||
|
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||||
|
if err == nil && code == http.StatusOK {
|
||||||
|
registry := model.Registry{}
|
||||||
|
if err := json.Unmarshal(body, ®istry); err != nil {
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
|
return ®istry, code, nil
|
||||||
|
}
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a testapi) RegistryList(authInfo usrInfo) ([]*model.Registry, int, error) {
|
||||||
|
_sling := sling.New().Base(a.basePath).Get("/api/registries")
|
||||||
|
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||||
|
if err != nil || code != http.StatusOK {
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var registries []*model.Registry
|
||||||
|
if err := json.Unmarshal(body, ®istries); err != nil {
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return registries, code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a testapi) RegistryCreate(authInfo usrInfo, registry *model.Registry) (int, error) {
|
||||||
|
_sling := sling.New().Base(a.basePath).Post("/api/registries").BodyJSON(registry)
|
||||||
|
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type pingReq struct {
|
||||||
|
ID *int64 `json:"id"`
|
||||||
|
Type *string `json:"type"`
|
||||||
|
URL *string `json:"url"`
|
||||||
|
CredentialType *string `json:"credential_type"`
|
||||||
|
AccessKey *string `json:"access_key"`
|
||||||
|
AccessSecret *string `json:"access_secret"`
|
||||||
|
Insecure *bool `json:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a testapi) RegistryPing(authInfo usrInfo, registry *pingReq) (int, error) {
|
||||||
|
_sling := sling.New().Base(a.basePath).Post("/api/registries/ping").BodyJSON(registry)
|
||||||
|
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a testapi) RegistryDelete(authInfo usrInfo, registryID int64) (int, error) {
|
||||||
|
_sling := sling.New().Base(a.basePath).Delete(fmt.Sprintf("/api/registries/%d", registryID))
|
||||||
|
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||||
|
if err != nil || code != http.StatusOK {
|
||||||
|
return code, fmt.Errorf("delete registry error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a testapi) RegistryUpdate(authInfo usrInfo, registryID int64, req *apimodels.RegistryUpdateRequest) (int, error) {
|
||||||
|
_sling := sling.New().Base(a.basePath).Put(fmt.Sprintf("/api/registries/%d", registryID)).BodyJSON(req)
|
||||||
|
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||||
|
if err != nil || code != http.StatusOK {
|
||||||
|
return code, fmt.Errorf("update registry error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
@ -24,9 +24,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
"github.com/goharbor/harbor/src/replication"
|
|
||||||
"github.com/goharbor/harbor/src/replication/core"
|
|
||||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LabelAPI handles requests for label management
|
// LabelAPI handles requests for label management
|
||||||
@ -332,9 +329,10 @@ func (l *LabelAPI) ListResources() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
result, err := core.GlobalController.GetPolicies(rep_models.QueryParameter{})
|
result, err := core.GlobalController.GetPolicies(rep_models.QueryParameter{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.SendInternalServerError(fmt.Errorf("failed to get policies: %v", err))
|
l.HandleInternalServerError(fmt.Sprintf("failed to get policies: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
policies := []*rep_models.ReplicationPolicy{}
|
policies := []*rep_models.ReplicationPolicy{}
|
||||||
@ -350,8 +348,9 @@ func (l *LabelAPI) ListResources() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
resources := map[string]interface{}{}
|
resources := map[string]interface{}{}
|
||||||
resources["replication_policies"] = policies
|
resources["replication_policies"] = nil
|
||||||
l.Data["json"] = resources
|
l.Data["json"] = resources
|
||||||
l.ServeJSON()
|
l.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/replication"
|
|
||||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -436,105 +433,3 @@ func TestLabelAPIDelete(t *testing.T) {
|
|||||||
|
|
||||||
runCodeCheckingCases(t, cases...)
|
runCodeCheckingCases(t, cases...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListResources(t *testing.T) {
|
|
||||||
// global level label
|
|
||||||
globalLabelID, err := dao.AddLabel(&models.Label{
|
|
||||||
Name: "globel_level_label",
|
|
||||||
Scope: common.LabelScopeGlobal,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer dao.DeleteLabel(globalLabelID)
|
|
||||||
|
|
||||||
// project level label
|
|
||||||
projectLabelID, err := dao.AddLabel(&models.Label{
|
|
||||||
Name: "project_level_label",
|
|
||||||
Scope: common.LabelScopeProject,
|
|
||||||
ProjectID: 1,
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer dao.DeleteLabel(projectLabelID)
|
|
||||||
|
|
||||||
targetID, err := dao.AddRepTarget(models.RepTarget{
|
|
||||||
Name: "target_for_testing_label_resource",
|
|
||||||
URL: "https://192.168.0.1",
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer dao.DeleteRepTarget(targetID)
|
|
||||||
|
|
||||||
// create a policy references both global and project labels
|
|
||||||
policyID, err := dao.AddRepPolicy(models.RepPolicy{
|
|
||||||
Name: "policy_for_testing_label_resource",
|
|
||||||
ProjectID: 1,
|
|
||||||
TargetID: targetID,
|
|
||||||
Trigger: fmt.Sprintf(`{"kind":"%s"}`, replication.TriggerKindManual),
|
|
||||||
Filters: fmt.Sprintf(`[{"kind":"%s","value":%d}, {"kind":"%s","value":%d}]`,
|
|
||||||
replication.FilterItemKindLabel, globalLabelID,
|
|
||||||
replication.FilterItemKindLabel, projectLabelID),
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer dao.DeleteRepPolicy(policyID)
|
|
||||||
|
|
||||||
cases := []*codeCheckingCase{
|
|
||||||
// 401
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d/resources", labelAPIBasePath, globalLabelID),
|
|
||||||
},
|
|
||||||
code: http.StatusUnauthorized,
|
|
||||||
},
|
|
||||||
// 404
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d/resources", labelAPIBasePath, 10000),
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
// 403: global level label
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d/resources", labelAPIBasePath, globalLabelID),
|
|
||||||
credential: projAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusForbidden,
|
|
||||||
},
|
|
||||||
// 403: project level label
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d/resources", labelAPIBasePath, projectLabelID),
|
|
||||||
credential: projDeveloper,
|
|
||||||
},
|
|
||||||
code: http.StatusForbidden,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
runCodeCheckingCases(t, cases...)
|
|
||||||
|
|
||||||
// 200: global level label
|
|
||||||
resources := map[string][]rep_models.ReplicationPolicy{}
|
|
||||||
err = handleAndParse(&testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d/resources", labelAPIBasePath, globalLabelID),
|
|
||||||
credential: sysAdmin,
|
|
||||||
}, &resources)
|
|
||||||
require.Nil(t, err)
|
|
||||||
policies := resources["replication_policies"]
|
|
||||||
require.Equal(t, 1, len(policies))
|
|
||||||
assert.Equal(t, policyID, policies[0].ID)
|
|
||||||
|
|
||||||
// 200: project level label
|
|
||||||
resources = map[string][]rep_models.ReplicationPolicy{}
|
|
||||||
err = handleAndParse(&testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d/resources", labelAPIBasePath, projectLabelID),
|
|
||||||
credential: projAdmin,
|
|
||||||
}, &resources)
|
|
||||||
require.Nil(t, err)
|
|
||||||
policies = resources["replication_policies"]
|
|
||||||
require.Equal(t, 1, len(policies))
|
|
||||||
assert.Equal(t, policyID, policies[0].ID)
|
|
||||||
}
|
|
||||||
|
15
src/core/api/models/execution.go
Normal file
15
src/core/api/models/execution.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execution defines the data model used in API level
|
||||||
|
type Execution struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TriggerMode string `json:"trigger_mode"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
SuccessRate string `json:"success_rate"`
|
||||||
|
StartTime time.Time `json:"start_time"`
|
||||||
|
}
|
12
src/core/api/models/registry.go
Normal file
12
src/core/api/models/registry.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// RegistryUpdateRequest is request used to update a registry.
|
||||||
|
type RegistryUpdateRequest struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
URL *string `json:"url"`
|
||||||
|
CredentialType *string `json:"credential_type"`
|
||||||
|
AccessKey *string `json:"access_key"`
|
||||||
|
AccessSecret *string `json:"access_secret"`
|
||||||
|
Insecure *bool `json:"insecure"`
|
||||||
|
}
|
@ -1,68 +0,0 @@
|
|||||||
// Copyright 2018 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 models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/astaxie/beego/validation"
|
|
||||||
common_models "github.com/goharbor/harbor/src/common/models"
|
|
||||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReplicationPolicy defines the data model used in API level
|
|
||||||
type ReplicationPolicy struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Filters []rep_models.Filter `json:"filters"`
|
|
||||||
ReplicateDeletion bool `json:"replicate_deletion"`
|
|
||||||
Trigger *rep_models.Trigger `json:"trigger"`
|
|
||||||
Projects []*common_models.Project `json:"projects"`
|
|
||||||
Targets []*common_models.RepTarget `json:"targets"`
|
|
||||||
CreationTime time.Time `json:"creation_time"`
|
|
||||||
UpdateTime time.Time `json:"update_time"`
|
|
||||||
ReplicateExistingImageNow bool `json:"replicate_existing_image_now"`
|
|
||||||
ErrorJobCount int64 `json:"error_job_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid ...
|
|
||||||
func (r *ReplicationPolicy) Valid(v *validation.Validation) {
|
|
||||||
if len(r.Name) == 0 {
|
|
||||||
v.SetError("name", "can not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Name) > 256 {
|
|
||||||
v.SetError("name", "max length is 256")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Projects) == 0 {
|
|
||||||
v.SetError("projects", "can not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Targets) == 0 {
|
|
||||||
v.SetError("targets", "can not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range r.Filters {
|
|
||||||
r.Filters[i].Valid(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Trigger == nil {
|
|
||||||
v.SetError("trigger", "can not be empty")
|
|
||||||
} else {
|
|
||||||
r.Trigger.Valid(v)
|
|
||||||
}
|
|
||||||
}
|
|
@ -118,7 +118,7 @@ func (p *ProjectAPI) Post() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if onlyAdmin && !p.SecurityCtx.IsSysAdmin() {
|
if onlyAdmin && !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) {
|
||||||
log.Errorf("Only sys admin can create project")
|
log.Errorf("Only sys admin can create project")
|
||||||
p.SendForbiddenError(errors.New("Only system admin can create project"))
|
p.SendForbiddenError(errors.New("Only system admin can create project"))
|
||||||
return
|
return
|
||||||
@ -159,9 +159,23 @@ func (p *ProjectAPI) Post() {
|
|||||||
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false)
|
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
owner := p.SecurityCtx.GetUsername()
|
||||||
|
// set the owner as the system admin when the API being called by replication
|
||||||
|
// it's a solution to workaround the restriction of project creation API:
|
||||||
|
// only normal users can create projects
|
||||||
|
if p.SecurityCtx.IsSolutionUser() {
|
||||||
|
user, err := dao.GetUser(models.User{
|
||||||
|
UserID: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
p.SendInternalServerError(fmt.Errorf("failed to get the user 1: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
owner = user.Username
|
||||||
|
}
|
||||||
projectID, err := p.ProjectMgr.Create(&models.Project{
|
projectID, err := p.ProjectMgr.Create(&models.Project{
|
||||||
Name: pro.Name,
|
Name: pro.Name,
|
||||||
OwnerName: p.SecurityCtx.GetUsername(),
|
OwnerName: owner,
|
||||||
Metadata: pro.Metadata,
|
Metadata: pro.Metadata,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -291,18 +305,6 @@ func (p *ProjectAPI) deletable(projectID int64) (*deletableResp, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
policies, err := dao.GetRepPolicyByProject(projectID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(policies) > 0 {
|
|
||||||
return &deletableResp{
|
|
||||||
Deletable: false,
|
|
||||||
Message: "the project contains replication rules, can not be deleted",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check helm charts number
|
// Check helm charts number
|
||||||
if config.WithChartMuseum() {
|
if config.WithChartMuseum() {
|
||||||
charts, err := chartController.ListCharts(p.project.Name)
|
charts, err := chartController.ListCharts(p.project.Name)
|
||||||
@ -531,8 +533,8 @@ func (p *ProjectAPI) Logs() {
|
|||||||
// TODO move this to pa ckage models
|
// TODO move this to pa ckage models
|
||||||
func validateProjectReq(req *models.ProjectRequest) error {
|
func validateProjectReq(req *models.ProjectRequest) error {
|
||||||
pn := req.Name
|
pn := req.Name
|
||||||
if utils.IsIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) {
|
if utils.IsIllegalLength(pn, projectNameMinLen, projectNameMaxLen) {
|
||||||
return fmt.Errorf("Project name is illegal in length. (greater than %d or less than %d)", projectNameMaxLen, projectNameMinLen)
|
return fmt.Errorf("Project name %s is illegal in length. (greater than %d or less than %d)", pn, projectNameMaxLen, projectNameMinLen)
|
||||||
}
|
}
|
||||||
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)
|
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)
|
||||||
legal := validProjectName.MatchString(pn)
|
legal := validProjectName.MatchString(pn)
|
||||||
|
496
src/core/api/registry.go
Normal file
496
src/core/api/registry.go
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
common_http "github.com/goharbor/harbor/src/common/http"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/api/models"
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
"github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/event"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/src/replication/policy"
|
||||||
|
"github.com/goharbor/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegistryAPI handles requests to /api/registries/{}. It manages registries integrated to Harbor.
|
||||||
|
type RegistryAPI struct {
|
||||||
|
BaseController
|
||||||
|
manager registry.Manager
|
||||||
|
policyCtl policy.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare validates the user
|
||||||
|
func (t *RegistryAPI) Prepare() {
|
||||||
|
t.BaseController.Prepare()
|
||||||
|
if !t.SecurityCtx.IsAuthenticated() {
|
||||||
|
t.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !t.SecurityCtx.IsSysAdmin() {
|
||||||
|
t.SendForbiddenError(errors.New(t.SecurityCtx.GetUsername()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.manager = replication.RegistryMgr
|
||||||
|
t.policyCtl = replication.PolicyCtl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping checks health status of a registry
|
||||||
|
func (t *RegistryAPI) Ping() {
|
||||||
|
req := struct {
|
||||||
|
ID *int64 `json:"id"`
|
||||||
|
Type *string `json:"type"`
|
||||||
|
URL *string `json:"url"`
|
||||||
|
CredentialType *string `json:"credential_type"`
|
||||||
|
AccessKey *string `json:"access_key"`
|
||||||
|
AccessSecret *string `json:"access_secret"`
|
||||||
|
Insecure *bool `json:"insecure"`
|
||||||
|
}{}
|
||||||
|
t.DecodeJSONReq(&req)
|
||||||
|
|
||||||
|
reg := &model.Registry{}
|
||||||
|
var err error
|
||||||
|
if req.ID != nil {
|
||||||
|
reg, err = t.manager.Get(*req.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.SendInternalServerError(fmt.Errorf("failed to get registry %d: %v", *req.ID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reg == nil {
|
||||||
|
t.SendNotFoundError(fmt.Errorf("registry %d not found", *req.ID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Type != nil {
|
||||||
|
reg.Type = model.RegistryType(*req.Type)
|
||||||
|
}
|
||||||
|
if req.URL != nil {
|
||||||
|
url, err := utils.ParseEndpoint(*req.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent SSRF security issue #3755
|
||||||
|
reg.URL = url.Scheme + "://" + url.Host + url.Path
|
||||||
|
}
|
||||||
|
if req.CredentialType != nil {
|
||||||
|
if reg.Credential == nil {
|
||||||
|
reg.Credential = &model.Credential{}
|
||||||
|
}
|
||||||
|
reg.Credential.Type = model.CredentialType(*req.CredentialType)
|
||||||
|
}
|
||||||
|
if req.AccessKey != nil {
|
||||||
|
if reg.Credential == nil {
|
||||||
|
reg.Credential = &model.Credential{}
|
||||||
|
}
|
||||||
|
reg.Credential.AccessKey = *req.AccessKey
|
||||||
|
}
|
||||||
|
if req.AccessSecret != nil {
|
||||||
|
if reg.Credential == nil {
|
||||||
|
reg.Credential = &model.Credential{}
|
||||||
|
}
|
||||||
|
reg.Credential.AccessSecret = *req.AccessSecret
|
||||||
|
}
|
||||||
|
if req.Insecure != nil {
|
||||||
|
reg.Insecure = *req.Insecure
|
||||||
|
}
|
||||||
|
if len(reg.Type) == 0 || len(reg.URL) == 0 {
|
||||||
|
t.SendBadRequestError(errors.New("type or url cannot be empty"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := registry.CheckHealthStatus(reg)
|
||||||
|
if err != nil {
|
||||||
|
e, ok := err.(*common_http.Error)
|
||||||
|
if ok && e.Code == http.StatusUnauthorized {
|
||||||
|
t.SendBadRequestError(errors.New("invalid credential"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.SendInternalServerError(fmt.Errorf("failed to check health of registry %s: %v", reg.URL, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != model.Healthy {
|
||||||
|
t.SendBadRequestError(errors.New(""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a registry by id.
|
||||||
|
func (t *RegistryAPI) Get() {
|
||||||
|
id, err := t.GetIDFromURL()
|
||||||
|
if err != nil {
|
||||||
|
t.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := t.manager.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get registry %d: %v", id, err)
|
||||||
|
t.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
t.SendNotFoundError(fmt.Errorf("registry %d not found", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide access secret
|
||||||
|
if r.Credential != nil && len(r.Credential.AccessSecret) != 0 {
|
||||||
|
r.Credential.AccessSecret = "*****"
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Data["json"] = r
|
||||||
|
t.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists all registries that match a given registry name.
|
||||||
|
func (t *RegistryAPI) List() {
|
||||||
|
name := t.GetString("name")
|
||||||
|
|
||||||
|
_, registries, err := t.manager.List(&model.RegistryQuery{
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to list registries %s: %v", name, err)
|
||||||
|
t.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide passwords
|
||||||
|
for _, r := range registries {
|
||||||
|
if r.Credential != nil && len(r.Credential.AccessSecret) != 0 {
|
||||||
|
r.Credential.AccessSecret = "*****"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Data["json"] = registries
|
||||||
|
t.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post creates a registry
|
||||||
|
func (t *RegistryAPI) Post() {
|
||||||
|
r := &model.Registry{}
|
||||||
|
isValid, err := t.DecodeJSONReqAndValidate(r)
|
||||||
|
if !isValid {
|
||||||
|
t.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reg, err := t.manager.GetByName(r.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get registry %s: %v", r.Name, err)
|
||||||
|
t.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reg != nil {
|
||||||
|
t.SendConflictError(fmt.Errorf("name '%s' is already used", r.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := registry.CheckHealthStatus(r)
|
||||||
|
if err != nil {
|
||||||
|
t.SendBadRequestError(fmt.Errorf("health check to registry %s failed: %v", r.URL, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status != model.Healthy {
|
||||||
|
t.SendBadRequestError(fmt.Errorf("registry %s is unhealthy: %s", r.URL, status))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := t.manager.Add(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Add registry '%s' error: %v", r.URL, err)
|
||||||
|
t.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put updates a registry
|
||||||
|
func (t *RegistryAPI) Put() {
|
||||||
|
id, err := t.GetIDFromURL()
|
||||||
|
if err != nil {
|
||||||
|
t.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := t.manager.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Get registry by id %d error: %v", id, err)
|
||||||
|
t.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
t.SendNotFoundError(fmt.Errorf("Registry %d not found", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := models.RegistryUpdateRequest{}
|
||||||
|
if err := t.DecodeJSONReq(&req); err != nil {
|
||||||
|
t.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
originalName := r.Name
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
r.Name = *req.Name
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
r.Description = *req.Description
|
||||||
|
}
|
||||||
|
if req.URL != nil {
|
||||||
|
r.URL = *req.URL
|
||||||
|
}
|
||||||
|
if req.CredentialType != nil {
|
||||||
|
r.Credential.Type = (model.CredentialType)(*req.CredentialType)
|
||||||
|
}
|
||||||
|
if req.AccessKey != nil {
|
||||||
|
r.Credential.AccessKey = *req.AccessKey
|
||||||
|
}
|
||||||
|
if req.AccessSecret != nil {
|
||||||
|
r.Credential.AccessSecret = *req.AccessSecret
|
||||||
|
}
|
||||||
|
if req.Insecure != nil {
|
||||||
|
r.Insecure = *req.Insecure
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Validate(r)
|
||||||
|
|
||||||
|
if r.Name != originalName {
|
||||||
|
reg, err := t.manager.GetByName(r.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Get registry by name '%s' error: %v", r.Name, err)
|
||||||
|
t.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reg != nil {
|
||||||
|
t.SendConflictError(errors.New("name is already used"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := registry.CheckHealthStatus(r)
|
||||||
|
if err != nil {
|
||||||
|
t.SendBadRequestError(fmt.Errorf("health check to registry %s failed: %v", r.URL, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status != model.Healthy {
|
||||||
|
t.SendBadRequestError(fmt.Errorf("registry %s is unhealthy: %s", r.URL, status))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.manager.Update(r); err != nil {
|
||||||
|
log.Errorf("Update registry %d error: %v", id, err)
|
||||||
|
t.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a registry
|
||||||
|
func (t *RegistryAPI) Delete() {
|
||||||
|
id, err := t.GetIDFromURL()
|
||||||
|
if err != nil {
|
||||||
|
t.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := t.manager.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Get registry %d error: %v", id, err)
|
||||||
|
log.Error(msg)
|
||||||
|
t.SendInternalServerError(errors.New(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if registry == nil {
|
||||||
|
t.SendNotFoundError(fmt.Errorf("Registry %d not found", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether there are replication policies that use this registry as source registry.
|
||||||
|
total, _, err := t.policyCtl.List([]*model.PolicyQuery{
|
||||||
|
{
|
||||||
|
SrcRegistry: id,
|
||||||
|
},
|
||||||
|
}...)
|
||||||
|
if err != nil {
|
||||||
|
t.SendInternalServerError(fmt.Errorf("List replication policies with source registry %d error: %v", id, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if total > 0 {
|
||||||
|
msg := fmt.Sprintf("Can't delete registry %d, %d replication policies use it as source registry", id, total)
|
||||||
|
log.Error(msg)
|
||||||
|
t.SendPreconditionFailedError(errors.New(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether there are replication policies that use this registry as destination registry.
|
||||||
|
total, _, err = t.policyCtl.List([]*model.PolicyQuery{
|
||||||
|
{
|
||||||
|
DestRegistry: id,
|
||||||
|
},
|
||||||
|
}...)
|
||||||
|
if err != nil {
|
||||||
|
t.SendInternalServerError(fmt.Errorf("List replication policies with destination registry %d error: %v", id, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if total > 0 {
|
||||||
|
msg := fmt.Sprintf("Can't delete registry %d, %d replication policies use it as destination registry", id, total)
|
||||||
|
log.Error(msg)
|
||||||
|
t.SendPreconditionFailedError(errors.New(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.manager.Remove(id); err != nil {
|
||||||
|
msg := fmt.Sprintf("Delete registry %d error: %v", id, err)
|
||||||
|
log.Error(msg)
|
||||||
|
t.SendPreconditionFailedError(errors.New(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo returns the base info and capability declarations of the registry
|
||||||
|
func (t *RegistryAPI) GetInfo() {
|
||||||
|
id, err := t.GetInt64FromPath(":id")
|
||||||
|
// "0" is used for the ID of the local Harbor registry
|
||||||
|
if err != nil || id < 0 {
|
||||||
|
t.SendBadRequestError(fmt.Errorf("invalid registry ID %s", t.GetString(":id")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var registry *model.Registry
|
||||||
|
if id == 0 {
|
||||||
|
registry = event.GetLocalRegistry()
|
||||||
|
} else {
|
||||||
|
registry, err = t.manager.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.SendInternalServerError(fmt.Errorf("failed to get registry %d: %v", id, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if registry == nil {
|
||||||
|
t.SendNotFoundError(fmt.Errorf("registry %d not found", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
factory, err := adapter.GetFactory(registry.Type)
|
||||||
|
if err != nil {
|
||||||
|
t.SendInternalServerError(fmt.Errorf("failed to get the adapter factory for registry type %s: %v", registry.Type, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adp, err := factory(registry)
|
||||||
|
if err != nil {
|
||||||
|
t.SendInternalServerError(fmt.Errorf("failed to create the adapter for registry %d: %v", registry.ID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, err := adp.Info()
|
||||||
|
if err != nil {
|
||||||
|
t.SendInternalServerError(fmt.Errorf("failed to get registry info %d: %v", id, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.WriteJSONData(process(info))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespace get the namespace of a registry
|
||||||
|
// TODO remove
|
||||||
|
func (t *RegistryAPI) GetNamespace() {
|
||||||
|
/*
|
||||||
|
var registry *model.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
id, err := t.GetInt64FromPath(":id")
|
||||||
|
if err != nil || id < 0 {
|
||||||
|
t.HandleBadRequest(fmt.Sprintf("invalid registry ID %s", t.GetString(":id")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if id > 0 {
|
||||||
|
registry, err = t.manager.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", id, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if id == 0 {
|
||||||
|
registry = event.GetLocalRegistry()
|
||||||
|
}
|
||||||
|
|
||||||
|
if registry == nil {
|
||||||
|
t.HandleNotFound(fmt.Sprintf("registry %d not found", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !adapter.HasFactory(registry.Type) {
|
||||||
|
t.HandleInternalServerError(fmt.Sprintf("no adapter factory found for %s", registry.Type))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
regFactory, err := adapter.GetFactory(registry.Type)
|
||||||
|
if err != nil {
|
||||||
|
t.HandleInternalServerError(fmt.Sprintf("fail to get adapter factory %s", registry.Type))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
regAdapter, err := regFactory(registry)
|
||||||
|
if err != nil {
|
||||||
|
t.HandleInternalServerError(fmt.Sprintf("fail to get adapter %s", registry.Type))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &model.NamespaceQuery{
|
||||||
|
Name: t.GetString("name"),
|
||||||
|
}
|
||||||
|
npResults, err := regAdapter.ListNamespaces(query)
|
||||||
|
if err != nil {
|
||||||
|
t.HandleInternalServerError(fmt.Sprintf("fail to list namespaces %s %v", registry.Type, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Data["json"] = npResults
|
||||||
|
t.ServeJSON()
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge "SupportedResourceTypes" into "SupportedResourceFilters" for UI to render easier
|
||||||
|
func process(info *model.RegistryInfo) *model.RegistryInfo {
|
||||||
|
if info == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
in := &model.RegistryInfo{
|
||||||
|
Type: info.Type,
|
||||||
|
Description: info.Description,
|
||||||
|
SupportedTriggers: info.SupportedTriggers,
|
||||||
|
}
|
||||||
|
filters := []*model.FilterStyle{}
|
||||||
|
for _, filter := range info.SupportedResourceFilters {
|
||||||
|
if filter.Type != model.FilterTypeResource {
|
||||||
|
filters = append(filters, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values := []string{}
|
||||||
|
for _, resourceType := range info.SupportedResourceTypes {
|
||||||
|
values = append(values, string(resourceType))
|
||||||
|
}
|
||||||
|
filters = append(filters, &model.FilterStyle{
|
||||||
|
Type: model.FilterTypeResource,
|
||||||
|
Style: model.FilterStyleTypeRadio,
|
||||||
|
Values: values,
|
||||||
|
})
|
||||||
|
in.SupportedResourceFilters = filters
|
||||||
|
|
||||||
|
return in
|
||||||
|
}
|
184
src/core/api/registry_test.go
Normal file
184
src/core/api/registry_test.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/core/api/models"
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
"github.com/goharbor/harbor/src/replication/dao"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testRegistry = &model.Registry{
|
||||||
|
Name: "test1",
|
||||||
|
URL: "https://registry-1.docker.io",
|
||||||
|
Type: "harbor",
|
||||||
|
Credential: nil,
|
||||||
|
}
|
||||||
|
testRegistry2 = &model.Registry{
|
||||||
|
Name: "test2",
|
||||||
|
URL: "https://registry-1.docker.io",
|
||||||
|
Type: "harbor",
|
||||||
|
Credential: nil,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegistrySuite struct {
|
||||||
|
suite.Suite
|
||||||
|
testAPI *testapi
|
||||||
|
defaultRegistry model.Registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) SetupSuite() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
assert.Nil(replication.Init(make(chan struct{})))
|
||||||
|
|
||||||
|
suite.testAPI = newHarborAPI()
|
||||||
|
code, err := suite.testAPI.RegistryCreate(*admin, testRegistry)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusCreated, code)
|
||||||
|
|
||||||
|
tmp, err := dao.GetRegistryByName(testRegistry.Name)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.NotNil(tmp)
|
||||||
|
suite.defaultRegistry = *testRegistry
|
||||||
|
suite.defaultRegistry.ID = tmp.ID
|
||||||
|
|
||||||
|
CommonAddUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) TearDownSuite() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
code, err := suite.testAPI.RegistryDelete(*admin, suite.defaultRegistry.ID)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusOK, code)
|
||||||
|
|
||||||
|
CommonDelUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) TestGet() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
|
||||||
|
// Get a non-existed registry
|
||||||
|
_, code, _ := suite.testAPI.RegistryGet(*admin, 0)
|
||||||
|
assert.Equal(http.StatusBadRequest, code)
|
||||||
|
|
||||||
|
// Get as admin, should succeed
|
||||||
|
retrieved, code, err := suite.testAPI.RegistryGet(*admin, suite.defaultRegistry.ID)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.NotNil(retrieved)
|
||||||
|
assert.Equal(http.StatusOK, code)
|
||||||
|
assert.Equal("test1", retrieved.Name)
|
||||||
|
|
||||||
|
// Get as user, should fail
|
||||||
|
_, code, _ = suite.testAPI.RegistryGet(*testUser, suite.defaultRegistry.ID)
|
||||||
|
assert.Equal(http.StatusForbidden, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) TestList() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
|
||||||
|
// List as admin, should succeed
|
||||||
|
registries, code, err := suite.testAPI.RegistryList(*admin)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusOK, code)
|
||||||
|
assert.Equal(1, len(registries))
|
||||||
|
|
||||||
|
// List as user, should fail
|
||||||
|
registries, code, err = suite.testAPI.RegistryList(*testUser)
|
||||||
|
assert.Equal(http.StatusForbidden, code)
|
||||||
|
assert.Equal(0, len(registries))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) TestPost() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
|
||||||
|
// Should conflict when create exited registry
|
||||||
|
code, err := suite.testAPI.RegistryCreate(*admin, testRegistry)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusConflict, code)
|
||||||
|
|
||||||
|
// Create as user, should fail
|
||||||
|
code, err = suite.testAPI.RegistryCreate(*testUser, testRegistry2)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusForbidden, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) TestPing() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
|
||||||
|
code, err := suite.testAPI.RegistryPing(*admin, &pingReq{
|
||||||
|
ID: &suite.defaultRegistry.ID,
|
||||||
|
})
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusOK, code)
|
||||||
|
|
||||||
|
var id int64 = -1
|
||||||
|
code, err = suite.testAPI.RegistryPing(*admin, &pingReq{
|
||||||
|
ID: &id,
|
||||||
|
})
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusNotFound, code)
|
||||||
|
|
||||||
|
code, err = suite.testAPI.RegistryPing(*admin, nil)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusBadRequest, code)
|
||||||
|
|
||||||
|
code, err = suite.testAPI.RegistryPing(*testUser, &pingReq{
|
||||||
|
ID: &suite.defaultRegistry.ID,
|
||||||
|
})
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusForbidden, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) TestRegistryPut() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
|
||||||
|
// Update as admin, should succeed
|
||||||
|
description := "foobar"
|
||||||
|
updateReq := &models.RegistryUpdateRequest{
|
||||||
|
Description: &description,
|
||||||
|
}
|
||||||
|
code, err := suite.testAPI.RegistryUpdate(*admin, suite.defaultRegistry.ID, updateReq)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusOK, code)
|
||||||
|
updated, code, err := suite.testAPI.RegistryGet(*admin, suite.defaultRegistry.ID)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusOK, code)
|
||||||
|
assert.Equal("foobar", updated.Description)
|
||||||
|
|
||||||
|
// Update as user, should fail
|
||||||
|
code, err = suite.testAPI.RegistryUpdate(*testUser, suite.defaultRegistry.ID, updateReq)
|
||||||
|
assert.NotNil(err)
|
||||||
|
assert.Equal(http.StatusForbidden, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RegistrySuite) TestDelete() {
|
||||||
|
assert := assert.New(suite.T())
|
||||||
|
|
||||||
|
code, err := suite.testAPI.RegistryCreate(*admin, testRegistry2)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusCreated, code)
|
||||||
|
|
||||||
|
tmp, err := dao.GetRegistryByName(testRegistry2.Name)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.NotNil(tmp)
|
||||||
|
|
||||||
|
// Delete as user, should fail
|
||||||
|
code, err = suite.testAPI.RegistryDelete(*testUser, tmp.ID)
|
||||||
|
assert.NotNil(err)
|
||||||
|
assert.Equal(http.StatusForbidden, code)
|
||||||
|
|
||||||
|
// Delete as admin, should succeed
|
||||||
|
code, err = suite.testAPI.RegistryDelete(*admin, tmp.ID)
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(http.StatusOK, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistrySuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(RegistrySuite))
|
||||||
|
}
|
@ -1,111 +0,0 @@
|
|||||||
// Copyright 2018 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"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
|
||||||
"github.com/goharbor/harbor/src/core/notifier"
|
|
||||||
"github.com/goharbor/harbor/src/replication/core"
|
|
||||||
"github.com/goharbor/harbor/src/replication/event/notification"
|
|
||||||
"github.com/goharbor/harbor/src/replication/event/topic"
|
|
||||||
|
|
||||||
"errors"
|
|
||||||
"github.com/docker/distribution/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReplicationAPI handles API calls for replication
|
|
||||||
type ReplicationAPI struct {
|
|
||||||
BaseController
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare does authentication and authorization works
|
|
||||||
func (r *ReplicationAPI) Prepare() {
|
|
||||||
r.BaseController.Prepare()
|
|
||||||
if !r.SecurityCtx.IsAuthenticated() {
|
|
||||||
r.SendUnAuthorizedError(errors.New("Unauthorized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !r.SecurityCtx.IsSysAdmin() && !r.SecurityCtx.IsSolutionUser() {
|
|
||||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post trigger a replication according to the specified policy
|
|
||||||
func (r *ReplicationAPI) Post() {
|
|
||||||
replication := &api_models.Replication{}
|
|
||||||
isValid, err := r.DecodeJSONReqAndValidate(replication)
|
|
||||||
if !isValid {
|
|
||||||
r.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
policy, err := core.GlobalController.GetPolicy(replication.PolicyID)
|
|
||||||
if err != nil {
|
|
||||||
r.SendInternalServerError(fmt.Errorf("failed to get replication policy %d: %v", replication.PolicyID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.ID == 0 {
|
|
||||||
r.SendNotFoundError(fmt.Errorf("replication policy %d not found", replication.PolicyID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err := dao.GetTotalCountOfRepJobs(&models.RepJobQuery{
|
|
||||||
PolicyID: replication.PolicyID,
|
|
||||||
Statuses: []string{models.JobPending, models.JobRunning},
|
|
||||||
Operations: []string{models.RepOpTransfer, models.RepOpDelete},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
r.SendInternalServerError(fmt.Errorf("failed to filter jobs of policy %d: %v",
|
|
||||||
replication.PolicyID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
r.SendPreconditionFailedError(errors.New("policy has running/pending jobs, new replication can not be triggered"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
opUUID, err := startReplication(replication.PolicyID)
|
|
||||||
if err != nil {
|
|
||||||
r.SendInternalServerError(fmt.Errorf("failed to publish replication topic for policy %d: %v", replication.PolicyID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("replication signal for policy %d sent", replication.PolicyID)
|
|
||||||
|
|
||||||
r.Data["json"] = api_models.ReplicationResponse{
|
|
||||||
UUID: opUUID,
|
|
||||||
}
|
|
||||||
r.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// startReplication triggers a replication and return the uuid of this replication.
|
|
||||||
func startReplication(policyID int64) (string, error) {
|
|
||||||
opUUID := strings.Replace(uuid.Generate().String(), "-", "", -1)
|
|
||||||
return opUUID, notifier.Publish(topic.StartReplicationTopic,
|
|
||||||
notification.StartReplicationNotification{
|
|
||||||
PolicyID: policyID,
|
|
||||||
Metadata: map[string]interface{}{
|
|
||||||
"op_uuid": opUUID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
46
src/core/api/replication_adapter.go
Normal file
46
src/core/api/replication_adapter.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright 2018 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 (
|
||||||
|
"errors"
|
||||||
|
"github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplicationAdapterAPI handles the replication adapter requests
|
||||||
|
type ReplicationAdapterAPI struct {
|
||||||
|
BaseController
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare ...
|
||||||
|
func (r *ReplicationAdapterAPI) Prepare() {
|
||||||
|
r.BaseController.Prepare()
|
||||||
|
if !r.SecurityCtx.IsSysAdmin() {
|
||||||
|
if !r.SecurityCtx.IsAuthenticated() {
|
||||||
|
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the replication adapters
|
||||||
|
func (r *ReplicationAdapterAPI) List() {
|
||||||
|
types := []model.RegistryType{}
|
||||||
|
types = append(types, adapter.ListRegisteredAdapterTypes()...)
|
||||||
|
r.WriteJSONData(types)
|
||||||
|
}
|
63
src/core/api/replication_adapter_test.go
Normal file
63
src/core/api/replication_adapter_test.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2018 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 (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakedFactory(*model.Registry) (adapter.Adapter, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplicationAdapterAPIList(t *testing.T) {
|
||||||
|
err := adapter.RegisterFactory("test", fakedFactory)
|
||||||
|
require.Nil(t, err)
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/adapters",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
277
src/core/api/replication_execution.go
Normal file
277
src/core/api/replication_execution.go
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||||
|
"github.com/goharbor/harbor/src/replication/event"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplicationOperationAPI handles the replication operation requests
|
||||||
|
type ReplicationOperationAPI struct {
|
||||||
|
BaseController
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare ...
|
||||||
|
func (r *ReplicationOperationAPI) Prepare() {
|
||||||
|
r.BaseController.Prepare()
|
||||||
|
// As we delegate the jobservice to trigger the scheduled replication,
|
||||||
|
// we need to allow the jobservice to call the API
|
||||||
|
if !(r.SecurityCtx.IsSysAdmin() || r.SecurityCtx.IsSolutionUser()) {
|
||||||
|
if !r.SecurityCtx.IsAuthenticated() {
|
||||||
|
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The API is open only for system admin currently, we can use
|
||||||
|
// the code commentted below to make the API available to the
|
||||||
|
// users who have permission for all projects that the policy
|
||||||
|
// refers
|
||||||
|
/*
|
||||||
|
func (r *ReplicationOperationAPI) authorized(policy *model.Policy, resource rbac.Resource, action rbac.Action) bool {
|
||||||
|
|
||||||
|
projects := []string{}
|
||||||
|
// pull mode
|
||||||
|
if policy.SrcRegistryID != 0 {
|
||||||
|
projects = append(projects, policy.DestNamespace)
|
||||||
|
} else {
|
||||||
|
// push mode
|
||||||
|
projects = append(projects, policy.SrcNamespaces...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, project := range projects {
|
||||||
|
resource := rbac.NewProjectNamespace(project).Resource(resource)
|
||||||
|
if !r.SecurityCtx.Can(action, resource) {
|
||||||
|
r.HandleForbidden(r.SecurityCtx.GetUsername())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ListExecutions ...
|
||||||
|
func (r *ReplicationOperationAPI) ListExecutions() {
|
||||||
|
query := &models.ExecutionQuery{
|
||||||
|
Trigger: r.GetString("trigger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.GetString("status")) > 0 {
|
||||||
|
query.Statuses = []string{r.GetString("status")}
|
||||||
|
}
|
||||||
|
if len(r.GetString("policy_id")) > 0 {
|
||||||
|
policyID, err := r.GetInt64("policy_id")
|
||||||
|
if err != nil || policyID <= 0 {
|
||||||
|
r.SendBadRequestError(fmt.Errorf("invalid policy_id %s", r.GetString("policy_id")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.PolicyID = policyID
|
||||||
|
}
|
||||||
|
page, size, err := r.GetPaginationParams()
|
||||||
|
if err != nil {
|
||||||
|
r.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.Page = page
|
||||||
|
query.Size = size
|
||||||
|
|
||||||
|
total, executions, err := replication.OperationCtl.ListExecutions(query)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to list executions: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.SetPaginationHeader(total, query.Page, query.Size)
|
||||||
|
r.WriteJSONData(executions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateExecution starts a replication
|
||||||
|
func (r *ReplicationOperationAPI) CreateExecution() {
|
||||||
|
execution := &models.Execution{}
|
||||||
|
if err := r.DecodeJSONReq(execution); err != nil {
|
||||||
|
r.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policy, err := replication.PolicyCtl.Get(execution.PolicyID)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", execution.PolicyID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if policy == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("policy %d not found", execution.PolicyID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !policy.Enabled {
|
||||||
|
r.SendBadRequestError(fmt.Errorf("the policy %d is disabled", execution.PolicyID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = event.PopulateRegistries(replication.RegistryMgr, policy); err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", execution.PolicyID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger := r.GetString("trigger", string(model.TriggerTypeManual))
|
||||||
|
executionID, err := replication.OperationCtl.StartReplication(policy, nil, model.TriggerType(trigger))
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to start replication for policy %d: %v", execution.PolicyID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Redirect(http.StatusCreated, strconv.FormatInt(executionID, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExecution gets one execution of the replication
|
||||||
|
func (r *ReplicationOperationAPI) GetExecution() {
|
||||||
|
executionID, err := r.GetInt64FromPath(":id")
|
||||||
|
if err != nil || executionID <= 0 {
|
||||||
|
r.SendBadRequestError(errors.New("invalid execution ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
execution, err := replication.OperationCtl.GetExecution(executionID)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if execution == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.WriteJSONData(execution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopExecution stops one execution of the replication
|
||||||
|
func (r *ReplicationOperationAPI) StopExecution() {
|
||||||
|
executionID, err := r.GetInt64FromPath(":id")
|
||||||
|
if err != nil || executionID <= 0 {
|
||||||
|
r.SendBadRequestError(errors.New("invalid execution ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
execution, err := replication.OperationCtl.GetExecution(executionID)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if execution == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := replication.OperationCtl.StopReplication(executionID); err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to stop execution %d: %v", executionID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTasks ...
|
||||||
|
func (r *ReplicationOperationAPI) ListTasks() {
|
||||||
|
executionID, err := r.GetInt64FromPath(":id")
|
||||||
|
if err != nil || executionID <= 0 {
|
||||||
|
r.SendBadRequestError(errors.New("invalid execution ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
execution, err := replication.OperationCtl.GetExecution(executionID)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if execution == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &models.TaskQuery{
|
||||||
|
ExecutionID: executionID,
|
||||||
|
ResourceType: r.GetString("resource_type"),
|
||||||
|
}
|
||||||
|
status := r.GetString("status")
|
||||||
|
if len(status) > 0 {
|
||||||
|
query.Statuses = []string{status}
|
||||||
|
}
|
||||||
|
page, size, err := r.GetPaginationParams()
|
||||||
|
if err != nil {
|
||||||
|
r.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.Page = page
|
||||||
|
query.Size = size
|
||||||
|
total, tasks, err := replication.OperationCtl.ListTasks(query)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to list tasks: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.SetPaginationHeader(total, query.Page, query.Size)
|
||||||
|
r.WriteJSONData(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaskLog ...
|
||||||
|
func (r *ReplicationOperationAPI) GetTaskLog() {
|
||||||
|
executionID, err := r.GetInt64FromPath(":id")
|
||||||
|
if err != nil || executionID <= 0 {
|
||||||
|
r.SendBadRequestError(errors.New("invalid execution ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
execution, err := replication.OperationCtl.GetExecution(executionID)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get execution %d: %v", executionID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if execution == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("execution %d not found", executionID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskID, err := r.GetInt64FromPath(":tid")
|
||||||
|
if err != nil || taskID <= 0 {
|
||||||
|
r.SendBadRequestError(errors.New("invalid task ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, err := replication.OperationCtl.GetTask(taskID)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get task %d: %v", taskID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if task == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("task %d not found", taskID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logBytes, err := replication.OperationCtl.GetTaskLog(taskID)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get log of task %d: %v", taskID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
|
||||||
|
r.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
|
||||||
|
_, err = r.Ctx.ResponseWriter.Write(logBytes)
|
||||||
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to write log of task %d: %v", taskID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
429
src/core/api/replication_execution_test.go
Normal file
429
src/core/api/replication_execution_test.go
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakedOperationController struct{}
|
||||||
|
|
||||||
|
func (f *fakedOperationController) StartReplication(policy *model.Policy, resource *model.Resource, trigger model.TriggerType) (int64, error) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
func (f *fakedOperationController) StopReplication(int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *fakedOperationController) ListExecutions(...*models.ExecutionQuery) (int64, []*models.Execution, error) {
|
||||||
|
return 1, []*models.Execution{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
PolicyID: 1,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
func (f *fakedOperationController) GetExecution(id int64) (*models.Execution, error) {
|
||||||
|
if id == 1 {
|
||||||
|
return &models.Execution{
|
||||||
|
ID: 1,
|
||||||
|
PolicyID: 1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedOperationController) ListTasks(...*models.TaskQuery) (int64, []*models.Task, error) {
|
||||||
|
return 1, []*models.Task{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
ExecutionID: 1,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
func (f *fakedOperationController) GetTask(id int64) (*models.Task, error) {
|
||||||
|
if id == 1 {
|
||||||
|
return &models.Task{
|
||||||
|
ID: 1,
|
||||||
|
ExecutionID: 1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedOperationController) UpdateTaskStatus(id int64, status string, statusCondition ...string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *fakedOperationController) GetTaskLog(int64) ([]byte, error) {
|
||||||
|
return []byte("success"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakedPolicyManager struct{}
|
||||||
|
|
||||||
|
func (f *fakedPolicyManager) Create(*model.Policy) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
func (f *fakedPolicyManager) List(...*model.PolicyQuery) (int64, []*model.Policy, error) {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedPolicyManager) Get(id int64) (*model.Policy, error) {
|
||||||
|
if id == 1 {
|
||||||
|
return &model.Policy{
|
||||||
|
ID: 1,
|
||||||
|
Enabled: true,
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if id == 2 {
|
||||||
|
return &model.Policy{
|
||||||
|
ID: 2,
|
||||||
|
Enabled: false,
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedPolicyManager) GetByName(name string) (*model.Policy, error) {
|
||||||
|
if name == "duplicate_name" {
|
||||||
|
return &model.Policy{
|
||||||
|
Name: "duplicate_name",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedPolicyManager) Update(*model.Policy) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *fakedPolicyManager) Remove(int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListExecutions(t *testing.T) {
|
||||||
|
operationCtl := replication.OperationCtl
|
||||||
|
defer func() {
|
||||||
|
replication.OperationCtl = operationCtl
|
||||||
|
}()
|
||||||
|
replication.OperationCtl = &fakedOperationController{}
|
||||||
|
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateExecution(t *testing.T) {
|
||||||
|
operationCtl := replication.OperationCtl
|
||||||
|
policyMgr := replication.PolicyCtl
|
||||||
|
registryMgr := replication.RegistryMgr
|
||||||
|
defer func() {
|
||||||
|
replication.OperationCtl = operationCtl
|
||||||
|
replication.PolicyCtl = policyMgr
|
||||||
|
replication.RegistryMgr = registryMgr
|
||||||
|
}()
|
||||||
|
replication.OperationCtl = &fakedOperationController{}
|
||||||
|
replication.PolicyCtl = &fakedPolicyManager{}
|
||||||
|
replication.RegistryMgr = &fakedRegistryManager{}
|
||||||
|
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
bodyJSON: &models.Execution{
|
||||||
|
PolicyID: 3,
|
||||||
|
},
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 400
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
bodyJSON: &models.Execution{
|
||||||
|
PolicyID: 2,
|
||||||
|
},
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
// 201
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPost,
|
||||||
|
url: "/api/replication/executions",
|
||||||
|
bodyJSON: &models.Execution{
|
||||||
|
PolicyID: 1,
|
||||||
|
},
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusCreated,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetExecution(t *testing.T) {
|
||||||
|
operationCtl := replication.OperationCtl
|
||||||
|
defer func() {
|
||||||
|
replication.OperationCtl = operationCtl
|
||||||
|
}()
|
||||||
|
replication.OperationCtl = &fakedOperationController{}
|
||||||
|
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/2",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
||||||
|
func TestStopExecution(t *testing.T) {
|
||||||
|
operationCtl := replication.OperationCtl
|
||||||
|
defer func() {
|
||||||
|
replication.OperationCtl = operationCtl
|
||||||
|
}()
|
||||||
|
replication.OperationCtl = &fakedOperationController{}
|
||||||
|
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/executions/1",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/executions/1",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/executions/2",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/executions/1",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListTasks(t *testing.T) {
|
||||||
|
operationCtl := replication.OperationCtl
|
||||||
|
defer func() {
|
||||||
|
replication.OperationCtl = operationCtl
|
||||||
|
}()
|
||||||
|
replication.OperationCtl = &fakedOperationController{}
|
||||||
|
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1/tasks",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1/tasks",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/2/tasks",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1/tasks",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTaskLog(t *testing.T) {
|
||||||
|
operationCtl := replication.OperationCtl
|
||||||
|
defer func() {
|
||||||
|
replication.OperationCtl = operationCtl
|
||||||
|
}()
|
||||||
|
replication.OperationCtl = &fakedOperationController{}
|
||||||
|
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1/tasks/1/log",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1/tasks/1/log",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404, execution not found
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/2/tasks/1/log",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 404, task not found
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1/tasks/2/log",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/executions/1/tasks/1/log",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runCodeCheckingCases(t, cases...)
|
||||||
|
}
|
@ -1,259 +0,0 @@
|
|||||||
// Copyright 2018 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 (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
common_job "github.com/goharbor/harbor/src/common/job"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
|
||||||
"github.com/goharbor/harbor/src/core/utils"
|
|
||||||
"github.com/goharbor/harbor/src/replication/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RepJobAPI handles request to /api/replicationJobs /api/replicationJobs/:id/log
|
|
||||||
type RepJobAPI struct {
|
|
||||||
BaseController
|
|
||||||
jobID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare validates that whether user has system admin role
|
|
||||||
func (ra *RepJobAPI) Prepare() {
|
|
||||||
ra.BaseController.Prepare()
|
|
||||||
if !ra.SecurityCtx.IsAuthenticated() {
|
|
||||||
ra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(ra.Ctx.Request.Method == http.MethodGet || ra.SecurityCtx.IsSysAdmin()) {
|
|
||||||
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ra.GetStringFromPath(":id")) != 0 {
|
|
||||||
id, err := ra.GetInt64FromPath(":id")
|
|
||||||
if err != nil {
|
|
||||||
ra.SendBadRequestError(fmt.Errorf("invalid ID: %s", ra.GetStringFromPath(":id")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ra.jobID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// List filters jobs according to the parameters
|
|
||||||
func (ra *RepJobAPI) List() {
|
|
||||||
|
|
||||||
policyID, err := ra.GetInt64("policy_id")
|
|
||||||
if err != nil || policyID <= 0 {
|
|
||||||
ra.SendBadRequestError(fmt.Errorf("invalid policy_id: %s", ra.GetString("policy_id")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
policy, err := core.GlobalController.GetPolicy(policyID)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get policy %d: %v", policyID, err)
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", policyID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.ID == 0 {
|
|
||||||
ra.SendNotFoundError(fmt.Errorf("policy %d not found", policyID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob)
|
|
||||||
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
|
|
||||||
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &models.RepJobQuery{
|
|
||||||
PolicyID: policyID,
|
|
||||||
// hide the schedule job, the schedule job is used to trigger replication
|
|
||||||
// for scheduled policy
|
|
||||||
Operations: []string{models.RepOpTransfer, models.RepOpDelete},
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Repository = ra.GetString("repository")
|
|
||||||
query.Statuses = ra.GetStrings("status")
|
|
||||||
query.OpUUID = ra.GetString("op_uuid")
|
|
||||||
|
|
||||||
startTimeStr := ra.GetString("start_time")
|
|
||||||
if len(startTimeStr) != 0 {
|
|
||||||
i, err := strconv.ParseInt(startTimeStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendBadRequestError(fmt.Errorf("invalid start_time: %s", startTimeStr))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t := time.Unix(i, 0)
|
|
||||||
query.StartTime = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
endTimeStr := ra.GetString("end_time")
|
|
||||||
if len(endTimeStr) != 0 {
|
|
||||||
i, err := strconv.ParseInt(endTimeStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendBadRequestError(fmt.Errorf("invalid end_time: %s", endTimeStr))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t := time.Unix(i, 0)
|
|
||||||
query.EndTime = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Page, query.Size, err = ra.GetPaginationParams()
|
|
||||||
if err != nil {
|
|
||||||
ra.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
total, err := dao.GetTotalCountOfRepJobs(query)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to get total count of repository jobs of policy %d: %v", policyID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jobs, err := dao.GetRepJobs(query)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to get repository jobs, query: %v :%v", query, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ra.SetPaginationHeader(total, query.Page, query.Size)
|
|
||||||
|
|
||||||
ra.Data["json"] = jobs
|
|
||||||
ra.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete ...
|
|
||||||
func (ra *RepJobAPI) Delete() {
|
|
||||||
if ra.jobID == 0 {
|
|
||||||
ra.SendBadRequestError(errors.New("ID is nil"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
job, err := dao.GetRepJob(ra.jobID)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get job %d: %v", ra.jobID, err)
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to get job %d: %v", ra.jobID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if job == nil {
|
|
||||||
ra.SendNotFoundError(fmt.Errorf("job %d not found", ra.jobID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if job.Status == models.JobPending || job.Status == models.JobRunning {
|
|
||||||
ra.SendBadRequestError(fmt.Errorf("job is %s, can not be deleted", job.Status))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = dao.DeleteRepJob(ra.jobID); err != nil {
|
|
||||||
log.Errorf("failed to deleted job %d: %v", ra.jobID, err)
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to deleted job %d: %v", ra.jobID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLog ...
|
|
||||||
func (ra *RepJobAPI) GetLog() {
|
|
||||||
if ra.jobID == 0 {
|
|
||||||
ra.SendBadRequestError(errors.New("ID is nil"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
job, err := dao.GetRepJob(ra.jobID)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to get replication job %d: %v", ra.jobID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if job == nil {
|
|
||||||
ra.SendNotFoundError(fmt.Errorf("replication job %d not found", ra.jobID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
policy, err := core.GlobalController.GetPolicy(job.PolicyID)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", job.PolicyID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob)
|
|
||||||
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
|
|
||||||
ra.SendForbiddenError(errors.New(ra.SecurityCtx.GetUsername()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logBytes, err := utils.GetJobServiceClient().GetJobLog(job.UUID)
|
|
||||||
if err != nil {
|
|
||||||
ra.ParseAndHandleError(fmt.Sprintf("failed to get log of job %s",
|
|
||||||
job.UUID), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ra.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
|
|
||||||
ra.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
|
|
||||||
_, err = ra.Ctx.ResponseWriter.Write(logBytes)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to write log of job %s: %v", job.UUID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopJobs stop replication jobs for the policy
|
|
||||||
func (ra *RepJobAPI) StopJobs() {
|
|
||||||
req := &api_models.StopJobsReq{}
|
|
||||||
isValid, err := ra.DecodeJSONReqAndValidate(req)
|
|
||||||
if !isValid {
|
|
||||||
ra.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
policy, err := core.GlobalController.GetPolicy(req.PolicyID)
|
|
||||||
if err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", req.PolicyID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.ID == 0 {
|
|
||||||
ra.SendNotFoundError(fmt.Errorf("policy %d not found", req.PolicyID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jobs, err := dao.GetRepJobs(&models.RepJobQuery{
|
|
||||||
PolicyID: policy.ID,
|
|
||||||
Operations: []string{models.RepOpTransfer, models.RepOpDelete},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to list jobs of policy %d: %v", policy.ID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, job := range jobs {
|
|
||||||
if err = utils.GetJobServiceClient().PostAction(job.UUID, common_job.JobActionStop); err != nil {
|
|
||||||
log.Errorf("failed to stop job id-%d uuid-%s: %v", job.ID, job.UUID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:add Post handler to call job service API to submit jobs by policy
|
|
@ -15,463 +15,243 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
common_model "github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
|
||||||
"github.com/goharbor/harbor/src/core/promgr"
|
|
||||||
"github.com/goharbor/harbor/src/replication"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
"github.com/goharbor/harbor/src/replication/core"
|
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
"github.com/goharbor/harbor/src/replication/event"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/src/replication/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement
|
// TODO rename the file to "replication.go"
|
||||||
type RepPolicyAPI struct {
|
|
||||||
|
// ReplicationPolicyAPI handles the replication policy requests
|
||||||
|
type ReplicationPolicyAPI struct {
|
||||||
BaseController
|
BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare validates whether the user has system admin role
|
// Prepare ...
|
||||||
func (pa *RepPolicyAPI) Prepare() {
|
func (r *ReplicationPolicyAPI) Prepare() {
|
||||||
pa.BaseController.Prepare()
|
r.BaseController.Prepare()
|
||||||
if !pa.SecurityCtx.IsAuthenticated() {
|
if !r.SecurityCtx.IsSysAdmin() {
|
||||||
pa.SendUnAuthorizedError(errors.New("Unauthorized"))
|
if !r.SecurityCtx.IsAuthenticated() {
|
||||||
|
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||||
if !(pa.Ctx.Request.Method == http.MethodGet || pa.SecurityCtx.IsSysAdmin()) {
|
|
||||||
pa.SendForbiddenError(errors.New(pa.SecurityCtx.GetUsername()))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get ...
|
// List the replication policies
|
||||||
func (pa *RepPolicyAPI) Get() {
|
func (r *ReplicationPolicyAPI) List() {
|
||||||
id, err := pa.GetIDFromURL()
|
page, size, err := r.GetPaginationParams()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pa.SendBadRequestError(err)
|
r.SendInternalServerError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
policy, err := core.GlobalController.GetPolicy(id)
|
// TODO: support more query
|
||||||
|
query := &model.PolicyQuery{
|
||||||
|
Name: r.GetString("name"),
|
||||||
|
Pagination: common_model.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Size: size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
total, policies, err := replication.PolicyCtl.List(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to get policy %d: %v", id, err)
|
r.SendInternalServerError(fmt.Errorf("failed to list policies: %v", err))
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", id, err))
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.ID == 0 {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
for _, policy := range policies {
|
||||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication)
|
if err = populateRegistries(replication.RegistryMgr, policy); err != nil {
|
||||||
if !pa.SecurityCtx.Can(rbac.ActionRead, resource) {
|
r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", policy.ID, err))
|
||||||
pa.SendForbiddenError(errors.New(pa.SecurityCtx.GetUsername()))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ply, err := convertFromRepPolicy(pa.ProjectMgr, policy)
|
|
||||||
if err != nil {
|
|
||||||
pa.ParseAndHandleError(fmt.Sprintf("failed to convert from replication policy"), err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
r.SetPaginationHeader(total, query.Page, query.Size)
|
||||||
pa.Data["json"] = ply
|
r.WriteJSONData(policies)
|
||||||
pa.ServeJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List ...
|
// Create the replication policy
|
||||||
func (pa *RepPolicyAPI) List() {
|
func (r *ReplicationPolicyAPI) Create() {
|
||||||
queryParam := rep_models.QueryParameter{
|
policy := &model.Policy{}
|
||||||
Name: pa.GetString("name"),
|
isValid, err := r.DecodeJSONReqAndValidate(policy)
|
||||||
}
|
|
||||||
projectIDStr := pa.GetString("project_id")
|
|
||||||
if len(projectIDStr) > 0 {
|
|
||||||
projectID, err := strconv.ParseInt(projectIDStr, 10, 64)
|
|
||||||
if err != nil || projectID <= 0 {
|
|
||||||
pa.SendBadRequestError(fmt.Errorf("invalid project ID: %s", projectIDStr))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
queryParam.ProjectID = projectID
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
queryParam.Page, queryParam.PageSize, err = pa.GetPaginationParams()
|
|
||||||
if err != nil {
|
|
||||||
pa.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := core.GlobalController.GetPolicies(queryParam)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get policies: %v, query parameters: %v", err, queryParam)
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get policies: %v, query parameters: %v", err, queryParam))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
policies := []*api_models.ReplicationPolicy{}
|
|
||||||
if result != nil {
|
|
||||||
total = result.Total
|
|
||||||
for _, policy := range result.Policies {
|
|
||||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication)
|
|
||||||
if !pa.SecurityCtx.Can(rbac.ActionRead, resource) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ply, err := convertFromRepPolicy(pa.ProjectMgr, *policy)
|
|
||||||
if err != nil {
|
|
||||||
pa.ParseAndHandleError(fmt.Sprintf("failed to convert from replication policy"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
policies = append(policies, ply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pa.SetPaginationHeader(total, queryParam.Page, queryParam.PageSize)
|
|
||||||
|
|
||||||
pa.Data["json"] = policies
|
|
||||||
pa.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post creates a replicartion policy
|
|
||||||
func (pa *RepPolicyAPI) Post() {
|
|
||||||
policy := &api_models.ReplicationPolicy{}
|
|
||||||
isValid, err := pa.DecodeJSONReqAndValidate(policy)
|
|
||||||
if !isValid {
|
if !isValid {
|
||||||
pa.SendBadRequestError(err)
|
r.SendBadRequestError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the name
|
if !r.validateName(policy) {
|
||||||
exist, err := exist(policy.Name)
|
return
|
||||||
|
}
|
||||||
|
if !r.validateRegistry(policy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := replication.PolicyCtl.Create(policy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to check the existence of policy %s: %v", policy.Name, err))
|
r.SendInternalServerError(fmt.Errorf("failed to create the policy: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
||||||
if exist {
|
|
||||||
pa.SendConflictError(fmt.Errorf("name %s is already used", policy.Name))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the existence of projects
|
|
||||||
for _, project := range policy.Projects {
|
|
||||||
pro, err := pa.ProjectMgr.Get(project.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
pa.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %d", project.ProjectID), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if pro == nil {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("project %d not found", project.ProjectID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
project.Name = pro.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the existence of targets
|
|
||||||
for _, target := range policy.Targets {
|
|
||||||
t, err := dao.GetRepTarget(target.ID)
|
|
||||||
if err != nil {
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get target %d: %v", target.ID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if t == nil {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("target %d not found", target.ID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the existence of labels
|
|
||||||
for _, filter := range policy.Filters {
|
|
||||||
if filter.Kind == replication.FilterItemKindLabel {
|
|
||||||
labelID := filter.Value.(int64)
|
|
||||||
label, err := dao.GetLabel(labelID)
|
|
||||||
if err != nil {
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get label %d: %v", labelID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if label == nil || label.Deleted {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("label %d not found", labelID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := core.GlobalController.CreatePolicy(convertToRepPolicy(policy))
|
|
||||||
if err != nil {
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to create policy: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.ReplicateExistingImageNow {
|
|
||||||
go func() {
|
|
||||||
if _, err = startReplication(id); err != nil {
|
|
||||||
log.Errorf("failed to send replication signal for policy %d: %v", id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("replication signal for policy %d sent", id)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
pa.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func exist(name string) (bool, error) {
|
// make sure the policy name doesn't exist
|
||||||
result, err := core.GlobalController.GetPolicies(rep_models.QueryParameter{
|
func (r *ReplicationPolicyAPI) validateName(policy *model.Policy) bool {
|
||||||
Name: name,
|
p, err := replication.PolicyCtl.GetByName(policy.Name)
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
r.SendInternalServerError(fmt.Errorf("failed to get policy %s: %v", policy.Name, err))
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
if p != nil {
|
||||||
for _, policy := range result.Policies {
|
r.SendConflictError(fmt.Errorf("policy %s already exists", policy.Name))
|
||||||
if policy.Name == name {
|
return false
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put updates the replication policy
|
// make sure the registry referred exists
|
||||||
func (pa *RepPolicyAPI) Put() {
|
func (r *ReplicationPolicyAPI) validateRegistry(policy *model.Policy) bool {
|
||||||
id, err := pa.GetIDFromURL()
|
var registryID int64
|
||||||
|
if policy.SrcRegistry != nil && policy.SrcRegistry.ID > 0 {
|
||||||
|
registryID = policy.SrcRegistry.ID
|
||||||
|
} else {
|
||||||
|
registryID = policy.DestRegistry.ID
|
||||||
|
}
|
||||||
|
registry, err := replication.RegistryMgr.Get(registryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pa.SendBadRequestError(err)
|
r.SendConflictError(fmt.Errorf("failed to get registry %d: %v", registryID, err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if registry == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("registry %d not found", registryID))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the specified replication policy
|
||||||
|
func (r *ReplicationPolicyAPI) Get() {
|
||||||
|
id, err := r.GetInt64FromPath(":id")
|
||||||
|
if id <= 0 || err != nil {
|
||||||
|
r.SendBadRequestError(errors.New("invalid policy ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalPolicy, err := core.GlobalController.GetPolicy(id)
|
policy, err := replication.PolicyCtl.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to get policy %d: %v", id, err)
|
r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", id, err))
|
return
|
||||||
|
}
|
||||||
|
if policy == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = populateRegistries(replication.RegistryMgr, policy); err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", policy.ID, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if originalPolicy.ID == 0 {
|
r.WriteJSONData(policy)
|
||||||
pa.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
}
|
||||||
|
|
||||||
|
// Update the replication policy
|
||||||
|
func (r *ReplicationPolicyAPI) Update() {
|
||||||
|
id, err := r.GetInt64FromPath(":id")
|
||||||
|
if id <= 0 || err != nil {
|
||||||
|
r.SendBadRequestError(errors.New("invalid policy ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
policy := &api_models.ReplicationPolicy{}
|
originalPolicy, err := replication.PolicyCtl.Get(id)
|
||||||
isValid, err := pa.DecodeJSONReqAndValidate(policy)
|
if err != nil {
|
||||||
|
r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if originalPolicy == nil {
|
||||||
|
r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policy := &model.Policy{}
|
||||||
|
isValid, err := r.DecodeJSONReqAndValidate(policy)
|
||||||
if !isValid {
|
if !isValid {
|
||||||
pa.SendBadRequestError(err)
|
r.SendBadRequestError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.Name != originalPolicy.Name &&
|
||||||
|
!r.validateName(policy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.validateRegistry(policy) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
policy.ID = id
|
policy.ID = id
|
||||||
|
if err := replication.PolicyCtl.Update(policy); err != nil {
|
||||||
// check the name
|
r.SendInternalServerError(fmt.Errorf("failed to update the policy %d: %v", id, err))
|
||||||
if policy.Name != originalPolicy.Name {
|
|
||||||
exist, err := exist(policy.Name)
|
|
||||||
if err != nil {
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to check the existence of policy %s: %v", policy.Name, err))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if exist {
|
|
||||||
pa.SendConflictError(fmt.Errorf("name %s is already used", policy.Name))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the existence of projects
|
|
||||||
for _, project := range policy.Projects {
|
|
||||||
pro, err := pa.ProjectMgr.Get(project.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
pa.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %d", project.ProjectID), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if pro == nil {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("project %d not found", project.ProjectID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
project.Name = pro.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the existence of targets
|
|
||||||
for _, target := range policy.Targets {
|
|
||||||
t, err := dao.GetRepTarget(target.ID)
|
|
||||||
if err != nil {
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get target %d: %v", target.ID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if t == nil {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("target %d not found", target.ID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the existence of labels
|
|
||||||
for _, filter := range policy.Filters {
|
|
||||||
if filter.Kind == replication.FilterItemKindLabel {
|
|
||||||
labelID := filter.Value.(int64)
|
|
||||||
label, err := dao.GetLabel(labelID)
|
|
||||||
if err != nil {
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get label %d: %v", labelID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if label == nil || label.Deleted {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("label %d not found", labelID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = core.GlobalController.UpdatePolicy(convertToRepPolicy(policy)); err != nil {
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to update policy %d: %v", id, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.ReplicateExistingImageNow {
|
|
||||||
go func() {
|
|
||||||
if _, err = startReplication(id); err != nil {
|
|
||||||
log.Errorf("failed to send replication signal for policy %d: %v", id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("replication signal for policy %d sent", id)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the replication policy
|
// Delete the replication policy
|
||||||
func (pa *RepPolicyAPI) Delete() {
|
func (r *ReplicationPolicyAPI) Delete() {
|
||||||
id, err := pa.GetIDFromURL()
|
id, err := r.GetInt64FromPath(":id")
|
||||||
if err != nil {
|
if id <= 0 || err != nil {
|
||||||
pa.SendBadRequestError(err)
|
r.SendBadRequestError(errors.New("invalid policy ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
policy, err := core.GlobalController.GetPolicy(id)
|
policy, err := replication.PolicyCtl.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to get policy %d: %v", id, err)
|
r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to get policy %d: %v", id, err))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if policy.ID == 0 {
|
|
||||||
pa.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err := dao.GetTotalCountOfRepJobs(&models.RepJobQuery{
|
|
||||||
PolicyID: id,
|
|
||||||
Statuses: []string{models.JobRunning, models.JobRetrying, models.JobPending},
|
|
||||||
// only get the transfer and delete jobs, do not get schedule job
|
|
||||||
Operations: []string{models.RepOpTransfer, models.RepOpDelete},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to filter jobs of policy %d: %v", id, err)
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to filter jobs of policy %d: %v", id, err))
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
pa.SendPreconditionFailedError(errors.New("policy has running/retrying/pending jobs, can not be deleted"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = core.GlobalController.RemovePolicy(id); err != nil {
|
|
||||||
log.Errorf("failed to delete policy %d: %v", id, err)
|
|
||||||
pa.SendInternalServerError(fmt.Errorf("failed to delete policy %d: %v", id, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertFromRepPolicy(projectMgr promgr.ProjectManager, policy rep_models.ReplicationPolicy) (*api_models.ReplicationPolicy, error) {
|
|
||||||
if policy.ID == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate simple properties
|
|
||||||
ply := &api_models.ReplicationPolicy{
|
|
||||||
ID: policy.ID,
|
|
||||||
Name: policy.Name,
|
|
||||||
Description: policy.Description,
|
|
||||||
ReplicateDeletion: policy.ReplicateDeletion,
|
|
||||||
Trigger: policy.Trigger,
|
|
||||||
CreationTime: policy.CreationTime,
|
|
||||||
UpdateTime: policy.UpdateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate projects
|
|
||||||
for _, projectID := range policy.ProjectIDs {
|
|
||||||
project, err := projectMgr.Get(projectID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ply.Projects = append(ply.Projects, project)
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate targets
|
|
||||||
for _, targetID := range policy.TargetIDs {
|
|
||||||
target, err := dao.GetRepTarget(targetID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
target.Password = ""
|
|
||||||
ply.Targets = append(ply.Targets, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate label used in label filter
|
|
||||||
for _, filter := range policy.Filters {
|
|
||||||
if filter.Kind == replication.FilterItemKindLabel {
|
|
||||||
labelID := filter.Value.(int64)
|
|
||||||
label, err := dao.GetLabel(labelID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
filter.Value = label
|
|
||||||
}
|
|
||||||
ply.Filters = append(ply.Filters, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO call the method from replication controller
|
|
||||||
errJobCount, err := dao.GetTotalCountOfRepJobs(&models.RepJobQuery{
|
|
||||||
PolicyID: policy.ID,
|
|
||||||
Statuses: []string{models.JobError},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ply.ErrorJobCount = errJobCount
|
|
||||||
|
|
||||||
return ply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertToRepPolicy(policy *api_models.ReplicationPolicy) rep_models.ReplicationPolicy {
|
|
||||||
if policy == nil {
|
if policy == nil {
|
||||||
return rep_models.ReplicationPolicy{}
|
r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ply := rep_models.ReplicationPolicy{
|
_, executions, err := replication.OperationCtl.ListExecutions(&models.ExecutionQuery{
|
||||||
ID: policy.ID,
|
PolicyID: id,
|
||||||
Name: policy.Name,
|
})
|
||||||
Description: policy.Description,
|
if err != nil {
|
||||||
Filters: policy.Filters,
|
r.SendInternalServerError(fmt.Errorf("failed to get the executions of policy %d: %v", id, err))
|
||||||
ReplicateDeletion: policy.ReplicateDeletion,
|
return
|
||||||
Trigger: policy.Trigger,
|
|
||||||
CreationTime: policy.CreationTime,
|
|
||||||
UpdateTime: policy.UpdateTime,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, project := range policy.Projects {
|
for _, execution := range executions {
|
||||||
ply.ProjectIDs = append(ply.ProjectIDs, project.ProjectID)
|
if execution.Status == models.ExecutionStatusInProgress {
|
||||||
ply.Namespaces = append(ply.Namespaces, project.Name)
|
r.SendInternalServerError(fmt.Errorf("the policy %d has running executions, can not be deleted", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, target := range policy.Targets {
|
if err := replication.PolicyCtl.Remove(id); err != nil {
|
||||||
ply.TargetIDs = append(ply.TargetIDs, target.ID)
|
r.SendInternalServerError(fmt.Errorf("failed to delete the policy %d: %v", id, err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return ply
|
|
||||||
|
// ignore the credential for the registries
|
||||||
|
func populateRegistries(registryMgr registry.Manager, policy *model.Policy) error {
|
||||||
|
if err := event.PopulateRegistries(registryMgr, policy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policy.SrcRegistry != nil {
|
||||||
|
policy.SrcRegistry.Credential = nil
|
||||||
|
}
|
||||||
|
if policy.DestRegistry != nil {
|
||||||
|
policy.DestRegistry.Credential = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -11,62 +11,101 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao/project"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
|
||||||
"github.com/goharbor/harbor/src/replication"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// TODO rename the file to "replication.go"
|
||||||
repPolicyAPIBasePath = "/api/policies/replication"
|
|
||||||
policyName = "testPolicy"
|
|
||||||
projectID int64 = 1
|
|
||||||
targetID int64
|
|
||||||
policyID int64
|
|
||||||
labelID2 int64
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRepPolicyAPIPost(t *testing.T) {
|
type fakedRegistryManager struct{}
|
||||||
postFunc := func(resp *httptest.ResponseRecorder) error {
|
|
||||||
id, err := parseResourceID(resp)
|
func (f *fakedRegistryManager) Add(*model.Registry) (int64, error) {
|
||||||
if err != nil {
|
return 0, nil
|
||||||
return err
|
}
|
||||||
|
func (f *fakedRegistryManager) List(...*model.RegistryQuery) (int64, []*model.Registry, error) {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedRegistryManager) Get(id int64) (*model.Registry, error) {
|
||||||
|
if id == 1 {
|
||||||
|
return &model.Registry{
|
||||||
|
Type: "faked_registry",
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
policyID = id
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedRegistryManager) GetByName(string) (*model.Registry, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (f *fakedRegistryManager) Update(*model.Registry, ...string) error {
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
func (f *fakedRegistryManager) Remove(int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (f *fakedRegistryManager) HealthCheck() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplicationPolicyAPIList(t *testing.T) {
|
||||||
|
policyMgr := replication.PolicyCtl
|
||||||
|
defer func() {
|
||||||
|
replication.PolicyCtl = policyMgr
|
||||||
|
}()
|
||||||
|
replication.PolicyCtl = &fakedPolicyManager{}
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/policies",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/policies",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 200
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/policies",
|
||||||
|
credential: sysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
CommonAddTarget()
|
runCodeCheckingCases(t, cases...)
|
||||||
targetID = int64(CommonGetTarget())
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
labelID2, err = dao.AddLabel(&models.Label{
|
|
||||||
Name: "label_for_replication_filter",
|
|
||||||
Scope: "g",
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer dao.DeleteLabel(labelID2)
|
|
||||||
|
|
||||||
|
func TestReplicationPolicyAPICreate(t *testing.T) {
|
||||||
|
policyMgr := replication.PolicyCtl
|
||||||
|
registryMgr := replication.RegistryMgr
|
||||||
|
defer func() {
|
||||||
|
replication.PolicyCtl = policyMgr
|
||||||
|
replication.RegistryMgr = registryMgr
|
||||||
|
}()
|
||||||
|
replication.PolicyCtl = &fakedPolicyManager{}
|
||||||
|
replication.RegistryMgr = &fakedRegistryManager{}
|
||||||
cases := []*codeCheckingCase{
|
cases := []*codeCheckingCase{
|
||||||
// 401
|
// 401
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
url: repPolicyAPIBasePath,
|
url: "/api/replication/policies",
|
||||||
},
|
},
|
||||||
code: http.StatusUnauthorized,
|
code: http.StatusUnauthorized,
|
||||||
},
|
},
|
||||||
@ -74,204 +113,64 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
|||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
url: repPolicyAPIBasePath,
|
url: "/api/replication/policies",
|
||||||
credential: nonSysAdmin,
|
credential: nonSysAdmin,
|
||||||
},
|
},
|
||||||
code: http.StatusForbidden,
|
code: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
// 400 empty policy name
|
||||||
// 400, invalid name
|
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
url: repPolicyAPIBasePath,
|
url: "/api/replication/policies",
|
||||||
bodyJSON: &api_models.ReplicationPolicy{},
|
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
// 400, invalid projects
|
// 400 empty registry
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
url: repPolicyAPIBasePath,
|
url: "/api/replication/policies",
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
Name: "policy01",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
// 400, invalid targets
|
// 409, duplicate policy name
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
url: repPolicyAPIBasePath,
|
url: "/api/replication/policies",
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
Name: "duplicate_name",
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
},
|
},
|
||||||
code: http.StatusBadRequest,
|
|
||||||
},
|
},
|
||||||
// 400, invalid filters
|
},
|
||||||
|
code: http.StatusConflict,
|
||||||
|
},
|
||||||
|
// 404, registry not found
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
url: repPolicyAPIBasePath,
|
url: "/api/replication/policies",
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: "invalid_filter_kind",
|
|
||||||
Pattern: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
},
|
bodyJSON: &model.Policy{
|
||||||
code: http.StatusBadRequest,
|
Name: "policy01",
|
||||||
},
|
SrcRegistry: &model.Registry{
|
||||||
// 400, invalid trigger
|
ID: 2,
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: "invalid_trigger_kind",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
// 404, project not found
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: replication.TriggerKindManual,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
// 404, target not found
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: replication.TriggerKindManual,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
// 404, label not found
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindLabel,
|
|
||||||
Value: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: replication.TriggerKindManual,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
},
|
||||||
code: http.StatusNotFound,
|
code: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
@ -279,340 +178,63 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
|||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPost,
|
method: http.MethodPost,
|
||||||
url: repPolicyAPIBasePath,
|
url: "/api/replication/policies",
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindLabel,
|
|
||||||
Value: labelID2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: replication.TriggerKindManual,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
Name: "policy01",
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: http.StatusCreated,
|
code: http.StatusCreated,
|
||||||
postFunc: postFunc,
|
|
||||||
},
|
|
||||||
// 409
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindLabel,
|
|
||||||
Value: labelID2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: replication.TriggerKindManual,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusConflict,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
runCodeCheckingCases(t, cases...)
|
runCodeCheckingCases(t, cases...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepPolicyAPIGet(t *testing.T) {
|
func TestReplicationPolicyAPIGet(t *testing.T) {
|
||||||
|
policyMgr := replication.PolicyCtl
|
||||||
|
registryMgr := replication.RegistryMgr
|
||||||
|
defer func() {
|
||||||
|
replication.PolicyCtl = policyMgr
|
||||||
|
replication.RegistryMgr = registryMgr
|
||||||
|
}()
|
||||||
|
replication.PolicyCtl = &fakedPolicyManager{}
|
||||||
|
replication.RegistryMgr = &fakedRegistryManager{}
|
||||||
cases := []*codeCheckingCase{
|
cases := []*codeCheckingCase{
|
||||||
// 404
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, 10000),
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
// 401
|
// 401
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, policyID),
|
url: "/api/replication/policies/1",
|
||||||
},
|
},
|
||||||
code: http.StatusUnauthorized,
|
code: http.StatusUnauthorized,
|
||||||
},
|
},
|
||||||
}
|
// 403
|
||||||
|
|
||||||
runCodeCheckingCases(t, cases...)
|
|
||||||
|
|
||||||
// 200
|
|
||||||
policy := &api_models.ReplicationPolicy{}
|
|
||||||
err := handleAndParse(
|
|
||||||
&testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, policyID),
|
|
||||||
credential: sysAdmin,
|
|
||||||
}, policy)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, policyID, policy.ID)
|
|
||||||
assert.Equal(t, policyName, policy.Name)
|
|
||||||
assert.Equal(t, 2, len(policy.Filters))
|
|
||||||
found := false
|
|
||||||
for _, filter := range policy.Filters {
|
|
||||||
if filter.Kind == replication.FilterItemKindLabel {
|
|
||||||
found = true
|
|
||||||
label, ok := filter.Value.(map[string]interface{})
|
|
||||||
if assert.True(t, ok) {
|
|
||||||
id := int64(label["id"].(float64))
|
|
||||||
deleted := label["deleted"].(bool)
|
|
||||||
assert.Equal(t, labelID2, id)
|
|
||||||
assert.True(t, deleted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, found)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepPolicyAPIList(t *testing.T) {
|
|
||||||
projectAdmin := models.User{
|
|
||||||
Username: "project_admin",
|
|
||||||
Password: "ProjectAdmin",
|
|
||||||
Email: "project_admin@test.com",
|
|
||||||
}
|
|
||||||
projectDev := models.User{
|
|
||||||
Username: "project_dev",
|
|
||||||
Password: "ProjectDev",
|
|
||||||
Email: "project_dev@test.com",
|
|
||||||
}
|
|
||||||
var proAdminPMID, proDevPMID int
|
|
||||||
proAdminID, err := dao.Register(projectAdmin)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer dao.DeleteUser(int(proAdminID))
|
|
||||||
if proAdminPMID, err = project.AddProjectMember(models.Member{
|
|
||||||
ProjectID: 1,
|
|
||||||
Role: models.PROJECTADMIN,
|
|
||||||
EntityID: int(proAdminID),
|
|
||||||
EntityType: common.UserMember,
|
|
||||||
}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer project.DeleteProjectMemberByID(proAdminPMID)
|
|
||||||
|
|
||||||
proDevID, err := dao.Register(projectDev)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer dao.DeleteUser(int(proDevID))
|
|
||||||
|
|
||||||
if proDevPMID, err = project.AddProjectMember(models.Member{
|
|
||||||
ProjectID: 1,
|
|
||||||
Role: models.DEVELOPER,
|
|
||||||
EntityID: int(proDevID),
|
|
||||||
EntityType: common.UserMember,
|
|
||||||
}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer project.DeleteProjectMemberByID(proDevPMID)
|
|
||||||
|
|
||||||
// 400: invalid project ID
|
|
||||||
runCodeCheckingCases(t, &codeCheckingCase{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
queryStruct: struct {
|
|
||||||
ProjectID int64 `url:"project_id"`
|
|
||||||
}{
|
|
||||||
ProjectID: -1,
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusBadRequest,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 200 system admin
|
|
||||||
policies := []*api_models.ReplicationPolicy{}
|
|
||||||
err = handleAndParse(
|
|
||||||
&testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
queryStruct: struct {
|
|
||||||
ProjectID int64 `url:"project_id"`
|
|
||||||
Name string `url:"name"`
|
|
||||||
}{
|
|
||||||
ProjectID: projectID,
|
|
||||||
Name: policyName,
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
}, &policies)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(policies))
|
|
||||||
assert.Equal(t, policyID, policies[0].ID)
|
|
||||||
assert.Equal(t, policyName, policies[0].Name)
|
|
||||||
|
|
||||||
// 200 project admin
|
|
||||||
policies = []*api_models.ReplicationPolicy{}
|
|
||||||
err = handleAndParse(
|
|
||||||
&testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
queryStruct: struct {
|
|
||||||
ProjectID int64 `url:"project_id"`
|
|
||||||
Name string `url:"name"`
|
|
||||||
}{
|
|
||||||
ProjectID: projectID,
|
|
||||||
Name: policyName,
|
|
||||||
},
|
|
||||||
credential: &usrInfo{
|
|
||||||
Name: projectAdmin.Username,
|
|
||||||
Passwd: projectAdmin.Password,
|
|
||||||
},
|
|
||||||
}, &policies)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 1, len(policies))
|
|
||||||
assert.Equal(t, policyID, policies[0].ID)
|
|
||||||
assert.Equal(t, policyName, policies[0].Name)
|
|
||||||
|
|
||||||
// 200 project developer
|
|
||||||
policies = []*api_models.ReplicationPolicy{}
|
|
||||||
err = handleAndParse(
|
|
||||||
&testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
queryStruct: struct {
|
|
||||||
ProjectID int64 `url:"project_id"`
|
|
||||||
Name string `url:"name"`
|
|
||||||
}{
|
|
||||||
ProjectID: projectID,
|
|
||||||
Name: policyName,
|
|
||||||
},
|
|
||||||
credential: &usrInfo{
|
|
||||||
Name: projectDev.Username,
|
|
||||||
Passwd: projectDev.Password,
|
|
||||||
},
|
|
||||||
}, &policies)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 0, len(policies))
|
|
||||||
|
|
||||||
// 200
|
|
||||||
policies = []*api_models.ReplicationPolicy{}
|
|
||||||
err = handleAndParse(
|
|
||||||
&testingRequest{
|
|
||||||
method: http.MethodGet,
|
|
||||||
url: repPolicyAPIBasePath,
|
|
||||||
queryStruct: struct {
|
|
||||||
ProjectID int64 `url:"project_id"`
|
|
||||||
Name string `url:"name"`
|
|
||||||
}{
|
|
||||||
ProjectID: projectID,
|
|
||||||
Name: "non_exist_policy",
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
}, &policies)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, 0, len(policies))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepPolicyAPIPut(t *testing.T) {
|
|
||||||
cases := []*codeCheckingCase{
|
|
||||||
// 404
|
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPut,
|
method: http.MethodGet,
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, 10000),
|
url: "/api/replication/policies/1",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404, policy not found
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodGet,
|
||||||
|
url: "/api/replication/policies/3",
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
},
|
},
|
||||||
code: http.StatusNotFound,
|
code: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
// 400, invalid trigger
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPut,
|
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, policyID),
|
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: "invalid_trigger_kind",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
|
||||||
},
|
|
||||||
code: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
// 200
|
// 200
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodPut,
|
method: http.MethodGet,
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, policyID),
|
url: "/api/replication/policies/1",
|
||||||
bodyJSON: &api_models.ReplicationPolicy{
|
|
||||||
Name: policyName,
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
|
||||||
ProjectID: projectID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: targetID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: replication.FilterItemKindRepository,
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: replication.TriggerKindImmediate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
},
|
},
|
||||||
code: http.StatusOK,
|
code: http.StatusOK,
|
||||||
@ -622,23 +244,99 @@ func TestRepPolicyAPIPut(t *testing.T) {
|
|||||||
runCodeCheckingCases(t, cases...)
|
runCodeCheckingCases(t, cases...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepPolicyAPIDelete(t *testing.T) {
|
func TestReplicationPolicyAPIUpdate(t *testing.T) {
|
||||||
|
policyMgr := replication.PolicyCtl
|
||||||
|
registryMgr := replication.RegistryMgr
|
||||||
|
defer func() {
|
||||||
|
replication.PolicyCtl = policyMgr
|
||||||
|
replication.RegistryMgr = registryMgr
|
||||||
|
}()
|
||||||
|
replication.PolicyCtl = &fakedPolicyManager{}
|
||||||
|
replication.RegistryMgr = &fakedRegistryManager{}
|
||||||
cases := []*codeCheckingCase{
|
cases := []*codeCheckingCase{
|
||||||
// 404
|
// 401
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodDelete,
|
method: http.MethodPut,
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, 10000),
|
url: "/api/replication/policies/1",
|
||||||
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/policies/1",
|
||||||
|
credential: nonSysAdmin,
|
||||||
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404 policy not found
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/policies/3",
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{},
|
||||||
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
// 400 empty policy name
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/policies/1",
|
||||||
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
// 409, duplicate policy name
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/policies/1",
|
||||||
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
Name: "duplicate_name",
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
code: http.StatusConflict,
|
||||||
|
},
|
||||||
|
// 404, registry not found
|
||||||
|
{
|
||||||
|
request: &testingRequest{
|
||||||
|
method: http.MethodPut,
|
||||||
|
url: "/api/replication/policies/1",
|
||||||
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
Name: "policy01",
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: http.StatusNotFound,
|
code: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
// 200
|
// 200
|
||||||
{
|
{
|
||||||
request: &testingRequest{
|
request: &testingRequest{
|
||||||
method: http.MethodDelete,
|
method: http.MethodPut,
|
||||||
url: fmt.Sprintf("%s/%d", repPolicyAPIBasePath, policyID),
|
url: "/api/replication/policies/1",
|
||||||
credential: sysAdmin,
|
credential: sysAdmin,
|
||||||
|
bodyJSON: &model.Policy{
|
||||||
|
Name: "policy01",
|
||||||
|
SrcRegistry: &model.Registry{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: http.StatusOK,
|
code: http.StatusOK,
|
||||||
},
|
},
|
||||||
@ -647,64 +345,49 @@ func TestRepPolicyAPIDelete(t *testing.T) {
|
|||||||
runCodeCheckingCases(t, cases...)
|
runCodeCheckingCases(t, cases...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertToRepPolicy(t *testing.T) {
|
func TestReplicationPolicyAPIDelete(t *testing.T) {
|
||||||
cases := []struct {
|
policyMgr := replication.PolicyCtl
|
||||||
input *api_models.ReplicationPolicy
|
defer func() {
|
||||||
expected rep_models.ReplicationPolicy
|
replication.PolicyCtl = policyMgr
|
||||||
}{
|
}()
|
||||||
|
replication.PolicyCtl = &fakedPolicyManager{}
|
||||||
|
cases := []*codeCheckingCase{
|
||||||
|
// 401
|
||||||
{
|
{
|
||||||
input: nil,
|
request: &testingRequest{
|
||||||
expected: rep_models.ReplicationPolicy{},
|
method: http.MethodDelete,
|
||||||
|
url: "/api/replication/policies/1",
|
||||||
},
|
},
|
||||||
|
code: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
// 403
|
||||||
{
|
{
|
||||||
input: &api_models.ReplicationPolicy{
|
request: &testingRequest{
|
||||||
ID: 1,
|
method: http.MethodDelete,
|
||||||
Name: "policy",
|
url: "/api/replication/policies/1",
|
||||||
Description: "description",
|
credential: nonSysAdmin,
|
||||||
Filters: []rep_models.Filter{
|
},
|
||||||
|
code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
// 404, policy not found
|
||||||
{
|
{
|
||||||
Kind: "filter_kind_01",
|
request: &testingRequest{
|
||||||
Pattern: "*",
|
method: http.MethodDelete,
|
||||||
|
url: "/api/replication/policies/3",
|
||||||
|
credential: sysAdmin,
|
||||||
},
|
},
|
||||||
|
code: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
ReplicateDeletion: true,
|
// 200
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: "trigger_kind_01",
|
|
||||||
},
|
|
||||||
Projects: []*models.Project{
|
|
||||||
{
|
{
|
||||||
ProjectID: 1,
|
request: &testingRequest{
|
||||||
Name: "library",
|
method: http.MethodDelete,
|
||||||
},
|
url: "/api/replication/policies/1",
|
||||||
},
|
credential: sysAdmin,
|
||||||
Targets: []*models.RepTarget{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: rep_models.ReplicationPolicy{
|
|
||||||
ID: 1,
|
|
||||||
Name: "policy",
|
|
||||||
Description: "description",
|
|
||||||
Filters: []rep_models.Filter{
|
|
||||||
{
|
|
||||||
Kind: "filter_kind_01",
|
|
||||||
Pattern: "*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ReplicateDeletion: true,
|
|
||||||
Trigger: &rep_models.Trigger{
|
|
||||||
Kind: "trigger_kind_01",
|
|
||||||
},
|
|
||||||
ProjectIDs: []int64{1},
|
|
||||||
Namespaces: []string{"library"},
|
|
||||||
TargetIDs: []int64{1},
|
|
||||||
},
|
},
|
||||||
|
code: http.StatusOK,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
runCodeCheckingCases(t, cases...)
|
||||||
assert.EqualValues(t, c.expected, convertToRepPolicy(c.input))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
// Copyright 2018 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/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
|
||||||
"github.com/goharbor/harbor/src/replication"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
replicationAPIBaseURL = "/api/replications"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReplicationAPIPost(t *testing.T) {
|
|
||||||
targetID, err := dao.AddRepTarget(
|
|
||||||
models.RepTarget{
|
|
||||||
Name: "test_replication_target",
|
|
||||||
URL: "127.0.0.1",
|
|
||||||
Username: "username",
|
|
||||||
Password: "password",
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer dao.DeleteRepTarget(targetID)
|
|
||||||
|
|
||||||
policyID, err := dao.AddRepPolicy(
|
|
||||||
models.RepPolicy{
|
|
||||||
Name: "test_replication_policy",
|
|
||||||
ProjectID: 1,
|
|
||||||
TargetID: targetID,
|
|
||||||
Trigger: fmt.Sprintf("{\"kind\":\"%s\"}", replication.TriggerKindManual),
|
|
||||||
})
|
|
||||||
require.Nil(t, err)
|
|
||||||
defer dao.DeleteRepPolicy(policyID)
|
|
||||||
|
|
||||||
cases := []*codeCheckingCase{
|
|
||||||
// 401
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: replicationAPIBaseURL,
|
|
||||||
bodyJSON: &api_models.Replication{
|
|
||||||
PolicyID: policyID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
code: http.StatusUnauthorized,
|
|
||||||
},
|
|
||||||
// 404
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: replicationAPIBaseURL,
|
|
||||||
bodyJSON: &api_models.Replication{
|
|
||||||
PolicyID: 10000,
|
|
||||||
},
|
|
||||||
credential: admin,
|
|
||||||
},
|
|
||||||
code: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
// 200
|
|
||||||
{
|
|
||||||
request: &testingRequest{
|
|
||||||
method: http.MethodPost,
|
|
||||||
url: replicationAPIBaseURL,
|
|
||||||
bodyJSON: &api_models.Replication{
|
|
||||||
PolicyID: policyID,
|
|
||||||
},
|
|
||||||
credential: admin,
|
|
||||||
},
|
|
||||||
code: http.StatusOK,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
runCodeCheckingCases(t, cases...)
|
|
||||||
}
|
|
@ -38,10 +38,10 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils/notary"
|
"github.com/goharbor/harbor/src/common/utils/notary"
|
||||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/core/notifier"
|
|
||||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||||
"github.com/goharbor/harbor/src/replication/event/notification"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
"github.com/goharbor/harbor/src/replication/event/topic"
|
"github.com/goharbor/harbor/src/replication/event"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
|
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
|
||||||
@ -330,15 +330,22 @@ func (ra *RepositoryAPI) Delete() {
|
|||||||
log.Infof("delete tag: %s:%s", repoName, t)
|
log.Infof("delete tag: %s:%s", repoName, t)
|
||||||
|
|
||||||
go func(tag string) {
|
go func(tag string) {
|
||||||
image := repoName + ":" + tag
|
e := &event.Event{
|
||||||
err := notifier.Publish(topic.ReplicationEventTopicOnDeletion, notification.OnDeletionNotification{
|
Type: event.EventTypeImagePush,
|
||||||
Image: image,
|
Resource: &model.Resource{
|
||||||
})
|
Type: model.ResourceTypeImage,
|
||||||
if err != nil {
|
Metadata: &model.ResourceMetadata{
|
||||||
log.Errorf("failed to publish on deletion topic for resource %s: %v", image, err)
|
Repository: &model.Repository{
|
||||||
return
|
Name: repoName,
|
||||||
|
},
|
||||||
|
Vtags: []string{tag},
|
||||||
|
},
|
||||||
|
Deleted: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := replication.EventHandler.Handle(e); err != nil {
|
||||||
|
log.Errorf("failed to handle event: %v", err)
|
||||||
}
|
}
|
||||||
log.Debugf("the on deletion topic for resource %s published", image)
|
|
||||||
}(t)
|
}(t)
|
||||||
|
|
||||||
go func(tag string) {
|
go func(tag string) {
|
||||||
|
@ -1,415 +0,0 @@
|
|||||||
// Copyright 2018 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 (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TargetAPI handles request to /api/targets/ping /api/targets/{}
|
|
||||||
type TargetAPI struct {
|
|
||||||
BaseController
|
|
||||||
secretKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare validates the user
|
|
||||||
func (t *TargetAPI) Prepare() {
|
|
||||||
t.BaseController.Prepare()
|
|
||||||
if !t.SecurityCtx.IsAuthenticated() {
|
|
||||||
t.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !t.SecurityCtx.IsSysAdmin() {
|
|
||||||
t.SendForbiddenError(errors.New(t.SecurityCtx.GetUsername()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
t.secretKey, err = config.SecretKey()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get secret key: %v", err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TargetAPI) ping(endpoint, username, password string, insecure bool) {
|
|
||||||
registry, err := newRegistryClient(endpoint, insecure, username, password)
|
|
||||||
if err == nil {
|
|
||||||
err = registry.Ping()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to ping target: %v", err)
|
|
||||||
// do not return any detail information of the error, or may cause SSRF security issue #3755
|
|
||||||
t.SendConflictError(errors.New("failed to ping target"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping validates whether the target is reachable and whether the credential is valid
|
|
||||||
func (t *TargetAPI) Ping() {
|
|
||||||
req := struct {
|
|
||||||
ID *int64 `json:"id"`
|
|
||||||
Endpoint *string `json:"endpoint"`
|
|
||||||
Username *string `json:"username"`
|
|
||||||
Password *string `json:"password"`
|
|
||||||
Insecure *bool `json:"insecure"`
|
|
||||||
}{}
|
|
||||||
if err := t.DecodeJSONReq(&req); err != nil {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target := &models.RepTarget{}
|
|
||||||
if req.ID != nil {
|
|
||||||
var err error
|
|
||||||
target, err = dao.GetRepTarget(*req.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.SendInternalServerError(fmt.Errorf("failed to get target %d: %v", *req.ID, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if target == nil {
|
|
||||||
t.SendNotFoundError(fmt.Errorf("target %d not found", *req.ID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(target.Password) != 0 {
|
|
||||||
target.Password, err = utils.ReversibleDecrypt(target.Password, t.secretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.SendInternalServerError(fmt.Errorf("failed to decrypt password: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Endpoint != nil {
|
|
||||||
url, err := utils.ParseEndpoint(*req.Endpoint)
|
|
||||||
if err != nil {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent SSRF security issue #3755
|
|
||||||
target.URL = url.Scheme + "://" + url.Host + url.Path
|
|
||||||
}
|
|
||||||
if req.Username != nil {
|
|
||||||
target.Username = *req.Username
|
|
||||||
}
|
|
||||||
if req.Password != nil {
|
|
||||||
target.Password = *req.Password
|
|
||||||
}
|
|
||||||
if req.Insecure != nil {
|
|
||||||
target.Insecure = *req.Insecure
|
|
||||||
}
|
|
||||||
|
|
||||||
t.ping(target.URL, target.Username, target.Password, target.Insecure)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get ...
|
|
||||||
func (t *TargetAPI) Get() {
|
|
||||||
id, err := t.GetIDFromURL()
|
|
||||||
if err != nil {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := dao.GetRepTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if target == nil {
|
|
||||||
t.SendNotFoundError(fmt.Errorf("target %d not found", id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target.Password = ""
|
|
||||||
|
|
||||||
t.Data["json"] = target
|
|
||||||
t.ServeJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// List ...
|
|
||||||
func (t *TargetAPI) List() {
|
|
||||||
name := t.GetString("name")
|
|
||||||
targets, err := dao.FilterRepTargets(name)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to filter targets %s: %v", name, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, target := range targets {
|
|
||||||
target.Password = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Data["json"] = targets
|
|
||||||
t.ServeJSON()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post ...
|
|
||||||
func (t *TargetAPI) Post() {
|
|
||||||
target := &models.RepTarget{}
|
|
||||||
isValid, err := t.DecodeJSONReqAndValidate(target)
|
|
||||||
if !isValid {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ta, err := dao.GetRepTargetByName(target.Name)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target %s: %v", target.Name, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ta != nil {
|
|
||||||
t.SendConflictError(errors.New("name is already used"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ta, err = dao.GetRepTargetByEndpoint(target.URL)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target [ %s ]: %v", target.URL, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ta != nil {
|
|
||||||
t.SendConflictError(fmt.Errorf("the target whose endpoint is %s already exists", target.URL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(target.Password) != 0 {
|
|
||||||
target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to encrypt password: %v", err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := dao.AddRepTarget(*target)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to add target: %v", err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put ...
|
|
||||||
func (t *TargetAPI) Put() {
|
|
||||||
id, err := t.GetIDFromURL()
|
|
||||||
if err != nil {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := dao.GetRepTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if target == nil {
|
|
||||||
t.SendNotFoundError(fmt.Errorf("target %d not found", id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(target.Password) != 0 {
|
|
||||||
target.Password, err = utils.ReversibleDecrypt(target.Password, t.secretKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to decrypt password: %v", err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req := struct {
|
|
||||||
Name *string `json:"name"`
|
|
||||||
Endpoint *string `json:"endpoint"`
|
|
||||||
Username *string `json:"username"`
|
|
||||||
Password *string `json:"password"`
|
|
||||||
Insecure *bool `json:"insecure"`
|
|
||||||
}{}
|
|
||||||
if err := t.DecodeJSONReq(&req); err != nil {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
originalName := target.Name
|
|
||||||
originalURL := target.URL
|
|
||||||
|
|
||||||
if req.Name != nil {
|
|
||||||
target.Name = *req.Name
|
|
||||||
}
|
|
||||||
if req.Endpoint != nil {
|
|
||||||
target.URL = *req.Endpoint
|
|
||||||
}
|
|
||||||
if req.Username != nil {
|
|
||||||
target.Username = *req.Username
|
|
||||||
}
|
|
||||||
if req.Password != nil {
|
|
||||||
target.Password = *req.Password
|
|
||||||
}
|
|
||||||
if req.Insecure != nil {
|
|
||||||
target.Insecure = *req.Insecure
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Validate(target)
|
|
||||||
|
|
||||||
if target.Name != originalName {
|
|
||||||
ta, err := dao.GetRepTargetByName(target.Name)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target %s: %v", target.Name, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ta != nil {
|
|
||||||
t.SendConflictError(errors.New("name is already used"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if target.URL != originalURL {
|
|
||||||
ta, err := dao.GetRepTargetByEndpoint(target.URL)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target [ %s ]: %v", target.URL, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ta != nil {
|
|
||||||
t.SendConflictError(fmt.Errorf("the target whose endpoint is %s already exists", target.URL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(target.Password) != 0 {
|
|
||||||
target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to encrypt password: %v", err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dao.UpdateRepTarget(*target); err != nil {
|
|
||||||
log.Errorf("failed to update target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete ...
|
|
||||||
func (t *TargetAPI) Delete() {
|
|
||||||
id, err := t.GetIDFromURL()
|
|
||||||
if err != nil {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := dao.GetRepTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if target == nil {
|
|
||||||
t.SendNotFoundError(fmt.Errorf("target %d not found", id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
policies, err := dao.GetRepPolicyByTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get policies according target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(policies) > 0 {
|
|
||||||
log.Error("the target is used by policies, can not be deleted")
|
|
||||||
t.SendPreconditionFailedError(errors.New("the target is used by policies, can not be deleted"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = dao.DeleteRepTarget(id); err != nil {
|
|
||||||
log.Errorf("failed to delete target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRegistryClient(endpoint string, insecure bool, username, password string) (*registry.Registry, error) {
|
|
||||||
transport := registry.GetHTTPTransport(insecure)
|
|
||||||
credential := auth.NewBasicAuthCredential(username, password)
|
|
||||||
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
}, credential)
|
|
||||||
return registry.NewRegistry(endpoint, &http.Client{
|
|
||||||
Transport: registry.NewTransport(transport, authorizer),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPolicies ...
|
|
||||||
func (t *TargetAPI) ListPolicies() {
|
|
||||||
id, err := t.GetIDFromURL()
|
|
||||||
if err != nil {
|
|
||||||
t.SendBadRequestError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := dao.GetRepTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if target == nil {
|
|
||||||
t.SendNotFoundError(fmt.Errorf("target %d not found", id))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
policies, err := dao.GetRepPolicyByTarget(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get policies according target %d: %v", id, err)
|
|
||||||
t.SendInternalServerError(errors.New(""))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Data["json"] = policies
|
|
||||||
t.ServeJSON()
|
|
||||||
}
|
|
@ -1,289 +0,0 @@
|
|||||||
// Copyright 2018 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"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
addTargetName = "testTargets"
|
|
||||||
)
|
|
||||||
|
|
||||||
var addTargetID int
|
|
||||||
|
|
||||||
func TestTargetsPost(t *testing.T) {
|
|
||||||
var httpStatusCode int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
assert := assert.New(t)
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
endPoint := os.Getenv("REGISTRY_URL")
|
|
||||||
repTargets := &apilib.RepTargetPost{Endpoint: endPoint, Name: addTargetName, Username: adminName, Password: adminPwd}
|
|
||||||
|
|
||||||
fmt.Println("Testing Targets Post API")
|
|
||||||
|
|
||||||
// -------------------case 1 : response code = 201------------------------//
|
|
||||||
fmt.Println("case 1 : response code = 201")
|
|
||||||
httpStatusCode, body, err := apiTest.AddTargets(*admin, *repTargets)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle add targets", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(201), httpStatusCode, "httpStatusCode should be 201")
|
|
||||||
t.Log(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------case 2 : response code = 409,name is already used-----------//
|
|
||||||
fmt.Println("case 2 : response code = 409,name is already used")
|
|
||||||
httpStatusCode, _, err = apiTest.AddTargets(*admin, *repTargets)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle add targets", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------case 3 : response code = 409,name is already used-----------//
|
|
||||||
fmt.Println("case 3 : response code = 409,endPoint is already used")
|
|
||||||
repTargets.Username = "errName"
|
|
||||||
httpStatusCode, _, err = apiTest.AddTargets(*admin, *repTargets)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle add targets", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------case 4 : response code = 401,User need to log in first.--------//
|
|
||||||
fmt.Println("case 4 : response code = 401,User need to log in first.")
|
|
||||||
httpStatusCode, _, err = apiTest.AddTargets(*unknownUsr, *repTargets)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle add targets", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetsGet(t *testing.T) {
|
|
||||||
var httpStatusCode int
|
|
||||||
var err error
|
|
||||||
var reslut []apilib.RepTarget
|
|
||||||
|
|
||||||
assert := assert.New(t)
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
fmt.Println("Testing Targets Get API")
|
|
||||||
|
|
||||||
// -------------------case 1 : response code = 200------------------------//
|
|
||||||
fmt.Println("case 1 : response code = 200")
|
|
||||||
httpStatusCode, reslut, err = apiTest.ListTargets(*admin, addTargetName)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle get targets", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
addTargetID = int(reslut[0].Id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetPing(t *testing.T) {
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
// 404: not exist target
|
|
||||||
target01 := struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}{
|
|
||||||
ID: 10000,
|
|
||||||
}
|
|
||||||
|
|
||||||
code, err := apiTest.PingTarget(*admin, target01)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, http.StatusNotFound, code)
|
|
||||||
|
|
||||||
// 400: empty endpoint
|
|
||||||
target02 := struct {
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
}{
|
|
||||||
Endpoint: "",
|
|
||||||
}
|
|
||||||
code, err = apiTest.PingTarget(*admin, target02)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, http.StatusBadRequest, code)
|
|
||||||
|
|
||||||
// 200
|
|
||||||
target03 := struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Insecure bool `json:"insecure"`
|
|
||||||
}{
|
|
||||||
ID: int64(addTargetID),
|
|
||||||
Endpoint: os.Getenv("REGISTRY_URL"),
|
|
||||||
Username: adminName,
|
|
||||||
Password: adminPwd,
|
|
||||||
Insecure: true,
|
|
||||||
}
|
|
||||||
code, err = apiTest.PingTarget(*admin, target03)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, http.StatusOK, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetGetByID(t *testing.T) {
|
|
||||||
var httpStatusCode int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
assert := assert.New(t)
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
fmt.Println("Testing Targets Get API by Id")
|
|
||||||
|
|
||||||
// -------------------case 1 : response code = 200------------------------//
|
|
||||||
fmt.Println("case 1 : response code = 200")
|
|
||||||
id := strconv.Itoa(addTargetID)
|
|
||||||
httpStatusCode, err = apiTest.GetTargetByID(*admin, id)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle get target by id", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------case 2 : response code = 404,target not found------------//
|
|
||||||
fmt.Println("case 2 : response code = 404,target not found")
|
|
||||||
id = "1111"
|
|
||||||
httpStatusCode, err = apiTest.GetTargetByID(*admin, id)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle get target by id", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetsPut(t *testing.T) {
|
|
||||||
var httpStatusCode int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
assert := assert.New(t)
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
endPoint := "1.1.1.1"
|
|
||||||
updateRepTargets := &apilib.RepTargetPost{Endpoint: endPoint, Name: addTargetName, Username: adminName, Password: adminPwd}
|
|
||||||
id := strconv.Itoa(addTargetID)
|
|
||||||
|
|
||||||
fmt.Println("Testing Target Put API")
|
|
||||||
|
|
||||||
// -------------------case 1 : response code = 200------------------------//
|
|
||||||
fmt.Println("case 1 : response code = 200")
|
|
||||||
httpStatusCode, err = apiTest.PutTargetByID(*admin, id, *updateRepTargets)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle update target", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------case 2 : response code = 404,target not found------------//
|
|
||||||
id = "111"
|
|
||||||
fmt.Println("case 2 : response code = 404,target not found")
|
|
||||||
httpStatusCode, err = apiTest.PutTargetByID(*admin, id, *updateRepTargets)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle update target", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestTargetGetPolicies(t *testing.T) {
|
|
||||||
var httpStatusCode int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
assert := assert.New(t)
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
fmt.Println("Testing Targets Get API to list policies")
|
|
||||||
|
|
||||||
// -------------------case 1 : response code = 200------------------------//
|
|
||||||
fmt.Println("case 1 : response code = 200")
|
|
||||||
id := strconv.Itoa(addTargetID)
|
|
||||||
httpStatusCode, err = apiTest.GetTargetPoliciesByID(*admin, id)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle get target by id", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------case 2 : response code = 404,target not found------------//
|
|
||||||
fmt.Println("case 2 : response code = 404,target not found")
|
|
||||||
id = "1111"
|
|
||||||
httpStatusCode, err = apiTest.GetTargetPoliciesByID(*admin, id)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle get target by id", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetsDelete(t *testing.T) {
|
|
||||||
var httpStatusCode int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
assert := assert.New(t)
|
|
||||||
apiTest := newHarborAPI()
|
|
||||||
|
|
||||||
id := strconv.Itoa(addTargetID)
|
|
||||||
fmt.Println("Testing Targets Delete API")
|
|
||||||
|
|
||||||
// -------------------case 1 : response code = 200------------------------//
|
|
||||||
fmt.Println("case 1 : response code = 200")
|
|
||||||
httpStatusCode, err = apiTest.DeleteTargetsByID(*admin, id)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle delete targets", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------case 2 : response code = 404,target not found------------//
|
|
||||||
fmt.Println("case 2 : response code = 404,target not found")
|
|
||||||
id = "1111"
|
|
||||||
httpStatusCode, err = apiTest.DeleteTargetsByID(*admin, id)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("Error whihle delete targets", err.Error())
|
|
||||||
t.Log(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -18,11 +18,12 @@ import (
|
|||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/astaxie/beego"
|
"github.com/astaxie/beego"
|
||||||
_ "github.com/astaxie/beego/session/redis"
|
_ "github.com/astaxie/beego/session/redis"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
@ -36,8 +37,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/core/filter"
|
"github.com/goharbor/harbor/src/core/filter"
|
||||||
"github.com/goharbor/harbor/src/core/proxy"
|
"github.com/goharbor/harbor/src/core/proxy"
|
||||||
"github.com/goharbor/harbor/src/core/service/token"
|
"github.com/goharbor/harbor/src/core/service/token"
|
||||||
"github.com/goharbor/harbor/src/replication/core"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
_ "github.com/goharbor/harbor/src/replication/event"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -70,6 +70,13 @@ func updateInitPassword(userID int, password string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gracefulShutdown(closing chan struct{}) {
|
||||||
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
log.Infof("capture system signal %s, to close \"closing\" channel", <-signals)
|
||||||
|
close(closing)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
beego.BConfig.WebConfig.Session.SessionOn = true
|
beego.BConfig.WebConfig.Session.SessionOn = true
|
||||||
beego.BConfig.WebConfig.Session.SessionName = "sid"
|
beego.BConfig.WebConfig.Session.SessionName = "sid"
|
||||||
@ -122,8 +129,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := core.Init(); err != nil {
|
closing := make(chan struct{})
|
||||||
log.Errorf("failed to initialize the replication controller: %v", err)
|
go gracefulShutdown(closing)
|
||||||
|
if err := replication.Init(closing); err != nil {
|
||||||
|
log.Fatalf("failed to init for replication: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filter.Init()
|
filter.Init()
|
||||||
|
@ -88,9 +88,6 @@ func initRouters() {
|
|||||||
beego.Router("/api/repositories/*/tags/:tag/manifest", &api.RepositoryAPI{}, "get:GetManifests")
|
beego.Router("/api/repositories/*/tags/:tag/manifest", &api.RepositoryAPI{}, "get:GetManifests")
|
||||||
beego.Router("/api/repositories/*/signatures", &api.RepositoryAPI{}, "get:GetSignatures")
|
beego.Router("/api/repositories/*/signatures", &api.RepositoryAPI{}, "get:GetSignatures")
|
||||||
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
|
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
|
||||||
beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List;put:StopJobs")
|
|
||||||
beego.Router("/api/jobs/replication/:id([0-9]+)", &api.RepJobAPI{})
|
|
||||||
beego.Router("/api/jobs/replication/:id([0-9]+)/log", &api.RepJobAPI{}, "get:GetLog")
|
|
||||||
beego.Router("/api/jobs/scan/:id([0-9]+)/log", &api.ScanJobAPI{}, "get:GetLog")
|
beego.Router("/api/jobs/scan/:id([0-9]+)/log", &api.ScanJobAPI{}, "get:GetLog")
|
||||||
|
|
||||||
beego.Router("/api/system/gc", &api.GCAPI{}, "get:List")
|
beego.Router("/api/system/gc", &api.GCAPI{}, "get:List")
|
||||||
@ -99,20 +96,20 @@ func initRouters() {
|
|||||||
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
|
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
|
||||||
beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post")
|
beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post")
|
||||||
|
|
||||||
beego.Router("/api/policies/replication/:id([0-9]+)", &api.RepPolicyAPI{})
|
|
||||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "get:List")
|
|
||||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
|
|
||||||
beego.Router("/api/targets/", &api.TargetAPI{}, "get:List")
|
|
||||||
beego.Router("/api/targets/", &api.TargetAPI{}, "post:Post")
|
|
||||||
beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{})
|
|
||||||
beego.Router("/api/targets/:id([0-9]+)/policies/", &api.TargetAPI{}, "get:ListPolicies")
|
|
||||||
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
|
|
||||||
beego.Router("/api/logs", &api.LogAPI{})
|
beego.Router("/api/logs", &api.LogAPI{})
|
||||||
|
|
||||||
|
beego.Router("/api/replication/adapters", &api.ReplicationAdapterAPI{}, "get:List")
|
||||||
|
beego.Router("/api/replication/executions", &api.ReplicationOperationAPI{}, "get:ListExecutions;post:CreateExecution")
|
||||||
|
beego.Router("/api/replication/executions/:id([0-9]+)", &api.ReplicationOperationAPI{}, "get:GetExecution;put:StopExecution")
|
||||||
|
beego.Router("/api/replication/executions/:id([0-9]+)/tasks", &api.ReplicationOperationAPI{}, "get:ListTasks")
|
||||||
|
beego.Router("/api/replication/executions/:id([0-9]+)/tasks/:tid([0-9]+)/log", &api.ReplicationOperationAPI{}, "get:GetTaskLog")
|
||||||
|
|
||||||
|
beego.Router("/api/replication/policies", &api.ReplicationPolicyAPI{}, "get:List;post:Create")
|
||||||
|
beego.Router("/api/replication/policies/:id([0-9]+)", &api.ReplicationPolicyAPI{}, "get:Get;put:Update;delete:Delete")
|
||||||
|
|
||||||
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
|
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
|
||||||
beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
|
beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
|
||||||
beego.Router("/api/statistics", &api.StatisticAPI{})
|
beego.Router("/api/statistics", &api.StatisticAPI{})
|
||||||
beego.Router("/api/replications", &api.ReplicationAPI{})
|
|
||||||
beego.Router("/api/labels", &api.LabelAPI{}, "post:Post;get:List")
|
beego.Router("/api/labels", &api.LabelAPI{}, "post:Post;get:List")
|
||||||
beego.Router("/api/labels/:id([0-9]+)", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete")
|
beego.Router("/api/labels/:id([0-9]+)", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete")
|
||||||
beego.Router("/api/labels/:id([0-9]+)/resources", &api.LabelAPI{}, "get:ListResources")
|
beego.Router("/api/labels/:id([0-9]+)/resources", &api.LabelAPI{}, "get:ListResources")
|
||||||
@ -128,10 +125,18 @@ func initRouters() {
|
|||||||
beego.Router("/service/notifications", ®istry.NotificationHandler{})
|
beego.Router("/service/notifications", ®istry.NotificationHandler{})
|
||||||
beego.Router("/service/notifications/clair", &clair.Handler{}, "post:Handle")
|
beego.Router("/service/notifications/clair", &clair.Handler{}, "post:Handle")
|
||||||
beego.Router("/service/notifications/jobs/scan/:id([0-9]+)", &jobs.Handler{}, "post:HandleScan")
|
beego.Router("/service/notifications/jobs/scan/:id([0-9]+)", &jobs.Handler{}, "post:HandleScan")
|
||||||
beego.Router("/service/notifications/jobs/replication/:id([0-9]+)", &jobs.Handler{}, "post:HandleReplication")
|
|
||||||
beego.Router("/service/notifications/jobs/adminjob/:id([0-9]+)", &admin.Handler{}, "post:HandleAdminJob")
|
beego.Router("/service/notifications/jobs/adminjob/:id([0-9]+)", &admin.Handler{}, "post:HandleAdminJob")
|
||||||
|
beego.Router("/service/notifications/jobs/replication/:id([0-9]+)", &jobs.Handler{}, "post:HandleReplicationScheduleJob")
|
||||||
|
beego.Router("/service/notifications/jobs/replication/task/:id([0-9]+)", &jobs.Handler{}, "post:HandleReplicationTask")
|
||||||
beego.Router("/service/token", &token.Handler{})
|
beego.Router("/service/token", &token.Handler{})
|
||||||
|
|
||||||
|
beego.Router("/api/registries", &api.RegistryAPI{}, "get:List;post:Post")
|
||||||
|
beego.Router("/api/registries/:id([0-9]+)", &api.RegistryAPI{}, "get:Get;put:Put;delete:Delete")
|
||||||
|
beego.Router("/api/registries/ping", &api.RegistryAPI{}, "post:Ping")
|
||||||
|
// we use "0" as the ID of the local Harbor registry, so don't add "([0-9]+)" in the path
|
||||||
|
beego.Router("/api/registries/:id/info", &api.RegistryAPI{}, "get:GetInfo")
|
||||||
|
beego.Router("/api/registries/:id/namespace", &api.RegistryAPI{}, "get:GetNamespace")
|
||||||
|
|
||||||
beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle")
|
beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle")
|
||||||
|
|
||||||
// APIs for chart repository
|
// APIs for chart repository
|
||||||
|
@ -23,6 +23,9 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/api"
|
"github.com/goharbor/harbor/src/core/api"
|
||||||
|
"github.com/goharbor/harbor/src/replication"
|
||||||
|
"github.com/goharbor/harbor/src/replication/operation/hook"
|
||||||
|
"github.com/goharbor/harbor/src/replication/policy/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
var statusMap = map[string]string{
|
var statusMap = map[string]string{
|
||||||
@ -40,6 +43,7 @@ type Handler struct {
|
|||||||
api.BaseController
|
api.BaseController
|
||||||
id int64
|
id int64
|
||||||
status string
|
status string
|
||||||
|
rawStatus string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare ...
|
// Prepare ...
|
||||||
@ -59,6 +63,7 @@ func (h *Handler) Prepare() {
|
|||||||
h.Abort("200")
|
h.Abort("200")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
h.rawStatus = data.Status
|
||||||
status, ok := statusMap[data.Status]
|
status, ok := statusMap[data.Status]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debugf("drop the job status update event: job id-%d, status-%s", id, status)
|
log.Debugf("drop the job status update event: job id-%d, status-%s", id, status)
|
||||||
@ -78,12 +83,22 @@ func (h *Handler) HandleScan() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleReplication handles the webhook of replication job
|
// HandleReplicationScheduleJob handles the webhook of replication schedule job
|
||||||
func (h *Handler) HandleReplication() {
|
func (h *Handler) HandleReplicationScheduleJob() {
|
||||||
log.Debugf("received replication job status update event: job-%d, status-%s", h.id, h.status)
|
log.Debugf("received replication schedule job status update event: schedule-job-%d, status-%s", h.id, h.status)
|
||||||
if err := dao.UpdateRepJobStatus(h.id, h.status); err != nil {
|
if err := scheduler.UpdateStatus(h.id, h.status); err != nil {
|
||||||
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
|
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
|
||||||
h.SendInternalServerError(err)
|
h.SendInternalServerError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleReplicationTask handles the webhook of replication task
|
||||||
|
func (h *Handler) HandleReplicationTask() {
|
||||||
|
log.Debugf("received replication task status update event: task-%d, status-%s", h.id, h.status)
|
||||||
|
if err := hook.UpdateTask(replication.OperationCtl, h.id, h.rawStatus); err != nil {
|
||||||
|
log.Errorf("Failed to update replication task status, id: %d, status: %s", h.id, h.status)
|
||||||
|
h.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -27,10 +27,11 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/api"
|
"github.com/goharbor/harbor/src/core/api"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/core/notifier"
|
|
||||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||||
rep_notification "github.com/goharbor/harbor/src/replication/event/notification"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
"github.com/goharbor/harbor/src/replication/event/topic"
|
"github.com/goharbor/harbor/src/replication/adapter"
|
||||||
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NotificationHandler handles request on /service/notifications/, which listens to registry's events.
|
// NotificationHandler handles request on /service/notifications/, which listens to registry's events.
|
||||||
@ -111,16 +112,24 @@ func (n *NotificationHandler) Post() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: handle image delete event and chart event
|
||||||
go func() {
|
go func() {
|
||||||
image := repository + ":" + tag
|
e := &rep_event.Event{
|
||||||
err := notifier.Publish(topic.ReplicationEventTopicOnPush, rep_notification.OnPushNotification{
|
Type: rep_event.EventTypeImagePush,
|
||||||
Image: image,
|
Resource: &model.Resource{
|
||||||
})
|
Type: model.ResourceTypeImage,
|
||||||
if err != nil {
|
Metadata: &model.ResourceMetadata{
|
||||||
log.Errorf("failed to publish on push topic for resource %s: %v", image, err)
|
Repository: &model.Repository{
|
||||||
return
|
Name: repository,
|
||||||
|
// TODO filling the metadata
|
||||||
|
},
|
||||||
|
Vtags: []string{tag},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := replication.EventHandler.Handle(e); err != nil {
|
||||||
|
log.Errorf("failed to handle event: %v", err)
|
||||||
}
|
}
|
||||||
log.Debugf("the on push topic for resource %s published", image)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if autoScanEnabled(pro) {
|
if autoScanEnabled(pro) {
|
||||||
@ -173,15 +182,16 @@ func filterEvents(notification *models.Notification) ([]*models.Event, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkEvent(event *models.Event) bool {
|
func checkEvent(event *models.Event) bool {
|
||||||
// pull and push manifest
|
// push action
|
||||||
if strings.ToLower(strings.TrimSpace(event.Request.UserAgent)) != "harbor-registry-client" && (event.Action == "pull" || event.Action == "push") {
|
if event.Action == "push" {
|
||||||
return true
|
|
||||||
}
|
|
||||||
// push manifest by job-service
|
|
||||||
if strings.ToLower(strings.TrimSpace(event.Request.UserAgent)) == "harbor-registry-client" && event.Action == "push" {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
// if it is pull action, check the user-agent
|
||||||
|
userAgent := strings.ToLower(strings.TrimSpace(event.Request.UserAgent))
|
||||||
|
if userAgent == "harbor-registry-client" || userAgent == strings.ToLower(adapter.UserAgentReplication) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoScanEnabled(project *models.Project) bool {
|
func autoScanEnabled(project *models.Project) bool {
|
||||||
|
@ -1,139 +0,0 @@
|
|||||||
// 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 replication
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
common_http "github.com/goharbor/harbor/src/common/http"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/env"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deleter deletes repository or images on the destination registry
|
|
||||||
type Deleter struct {
|
|
||||||
ctx env.JobContext
|
|
||||||
repository *repository
|
|
||||||
dstRegistry *registry
|
|
||||||
logger logger.Interface
|
|
||||||
retry bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldRetry : retry if the error is network error
|
|
||||||
func (d *Deleter) ShouldRetry() bool {
|
|
||||||
return d.retry
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxFails ...
|
|
||||||
func (d *Deleter) MaxFails() uint {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate ....
|
|
||||||
func (d *Deleter) Validate(params map[string]interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run ...
|
|
||||||
func (d *Deleter) Run(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
err := d.run(ctx, params)
|
|
||||||
d.retry = retry(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Deleter) run(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
if err := d.init(ctx, params); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Deleter) init(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
d.logger = ctx.GetLogger()
|
|
||||||
d.ctx = ctx
|
|
||||||
|
|
||||||
if canceled(d.ctx) {
|
|
||||||
d.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
|
|
||||||
d.repository = &repository{
|
|
||||||
name: params["repository"].(string),
|
|
||||||
}
|
|
||||||
if tags, ok := params["tags"]; ok {
|
|
||||||
tgs := tags.([]interface{})
|
|
||||||
for _, tg := range tgs {
|
|
||||||
d.repository.tags = append(d.repository.tags, tg.(string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
url := params["dst_registry_url"].(string)
|
|
||||||
insecure := params["dst_registry_insecure"].(bool)
|
|
||||||
cred := auth.NewBasicAuthCredential(
|
|
||||||
params["dst_registry_username"].(string),
|
|
||||||
params["dst_registry_password"].(string))
|
|
||||||
|
|
||||||
var err error
|
|
||||||
d.dstRegistry, err = initRegistry(url, insecure, cred, d.repository.name)
|
|
||||||
if err != nil {
|
|
||||||
d.logger.Errorf("failed to create client for destination registry: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, insecure: %v",
|
|
||||||
d.repository.name, d.repository.tags, d.dstRegistry.url, d.dstRegistry.insecure)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Deleter) delete() error {
|
|
||||||
repository := d.repository.name
|
|
||||||
tags := d.repository.tags
|
|
||||||
if len(tags) == 0 {
|
|
||||||
if canceled(d.ctx) {
|
|
||||||
d.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
if err := d.dstRegistry.DeleteRepository(repository); err != nil {
|
|
||||||
if e, ok := err.(*common_http.Error); ok && e.Code == http.StatusNotFound {
|
|
||||||
d.logger.Warningf("repository %s not found", repository)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
d.logger.Errorf("failed to delete repository %s: %v", repository, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.logger.Infof("repository %s has been deleted", repository)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tag := range tags {
|
|
||||||
if canceled(d.ctx) {
|
|
||||||
d.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
if err := d.dstRegistry.DeleteImage(repository, tag); err != nil {
|
|
||||||
if e, ok := err.(*common_http.Error); ok && e.Code == http.StatusNotFound {
|
|
||||||
d.logger.Warningf("image %s:%s not found", repository, tag)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
d.logger.Errorf("failed to delete image %s:%s: %v", repository, tag, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.logger.Infof("image %s:%s has been deleted", repository, tag)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
// 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 replication
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
common_http "github.com/goharbor/harbor/src/common/http"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
reg "github.com/goharbor/harbor/src/common/utils/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
type repository struct {
|
|
||||||
name string
|
|
||||||
tags []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// registry wraps operations of Harbor UI and docker registry into one struct
|
|
||||||
type registry struct {
|
|
||||||
reg.Repository // docker registry client
|
|
||||||
client *common_http.Client // Harbor client
|
|
||||||
url string
|
|
||||||
insecure bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registry) GetProject(name string) (*models.Project, error) {
|
|
||||||
url, err := url.Parse(strings.TrimRight(r.url, "/") + "/api/projects")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q := url.Query()
|
|
||||||
q.Set("name", name)
|
|
||||||
url.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
projects := []*models.Project{}
|
|
||||||
if err = r.client.Get(url.String(), &projects); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, project := range projects {
|
|
||||||
if project.Name == name {
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("project %s not found", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registry) CreateProject(project *models.Project) error {
|
|
||||||
// only replicate the public property of project
|
|
||||||
pro := struct {
|
|
||||||
models.ProjectRequest
|
|
||||||
Public int `json:"public"`
|
|
||||||
}{
|
|
||||||
ProjectRequest: models.ProjectRequest{
|
|
||||||
Name: project.Name,
|
|
||||||
Metadata: map[string]string{
|
|
||||||
models.ProMetaPublic: strconv.FormatBool(project.IsPublic()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// put "public" property in both metadata and public field to keep compatibility
|
|
||||||
// with old version API(<=1.2.0)
|
|
||||||
if project.IsPublic() {
|
|
||||||
pro.Public = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.client.Post(strings.TrimRight(r.url, "/")+"/api/projects/", pro)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registry) DeleteRepository(repository string) error {
|
|
||||||
return r.client.Delete(strings.TrimRight(r.url, "/") + "/api/repositories/" + repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registry) DeleteImage(repository, tag string) error {
|
|
||||||
return r.client.Delete(strings.TrimRight(r.url, "/") + "/api/repositories/" + repository + "/tags/" + tag)
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
// 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 replication
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
common_http "github.com/goharbor/harbor/src/common/http"
|
|
||||||
"github.com/goharbor/harbor/src/common/http/modifier/auth"
|
|
||||||
reg "github.com/goharbor/harbor/src/common/utils/registry"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/env"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Replicator call UI's API to start a repliation according to the policy ID
|
|
||||||
// passed in parameters
|
|
||||||
type Replicator struct {
|
|
||||||
ctx env.JobContext
|
|
||||||
url string // the URL of UI service
|
|
||||||
insecure bool
|
|
||||||
policyID int64
|
|
||||||
client *common_http.Client
|
|
||||||
logger logger.Interface
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldRetry ...
|
|
||||||
func (r *Replicator) ShouldRetry() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxFails ...
|
|
||||||
func (r *Replicator) MaxFails() uint {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate ....
|
|
||||||
func (r *Replicator) Validate(params map[string]interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run ...
|
|
||||||
func (r *Replicator) Run(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
if err := r.init(ctx, params); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.replicate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Replicator) init(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
r.logger = ctx.GetLogger()
|
|
||||||
r.ctx = ctx
|
|
||||||
if canceled(r.ctx) {
|
|
||||||
r.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
|
|
||||||
r.policyID = (int64)(params["policy_id"].(float64))
|
|
||||||
r.url = params["url"].(string)
|
|
||||||
r.insecure = params["insecure"].(bool)
|
|
||||||
cred := auth.NewSecretAuthorizer(secret())
|
|
||||||
|
|
||||||
r.client = common_http.NewClient(&http.Client{
|
|
||||||
Transport: reg.GetHTTPTransport(r.insecure),
|
|
||||||
}, cred)
|
|
||||||
|
|
||||||
r.logger.Infof("initialization completed: policy ID: %d, URL: %s, insecure: %v",
|
|
||||||
r.policyID, r.url, r.insecure)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Replicator) replicate() error {
|
|
||||||
if err := r.client.Post(fmt.Sprintf("%s/api/replications", r.url), struct {
|
|
||||||
PolicyID int64 `json:"policy_id"`
|
|
||||||
}{
|
|
||||||
PolicyID: r.policyID,
|
|
||||||
}); err != nil {
|
|
||||||
r.logger.Errorf("failed to send the replication request to %s: %v", r.url, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.logger.Info("the replication request has been sent successfully")
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
113
src/jobservice/job/impl/replication/replication.go
Normal file
113
src/jobservice/job/impl/replication/replication.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// 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 replication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/env"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/opm"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/src/replication/transfer"
|
||||||
|
|
||||||
|
// import chart transfer
|
||||||
|
_ "github.com/goharbor/harbor/src/replication/transfer/chart"
|
||||||
|
// import image transfer
|
||||||
|
_ "github.com/goharbor/harbor/src/replication/transfer/image"
|
||||||
|
// register the Harbor adapter
|
||||||
|
_ "github.com/goharbor/harbor/src/replication/adapter/harbor"
|
||||||
|
// register the DockerHub adapter
|
||||||
|
_ "github.com/goharbor/harbor/src/replication/adapter/dockerhub"
|
||||||
|
// register the Native adapter
|
||||||
|
_ "github.com/goharbor/harbor/src/replication/adapter/native"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replication implements the job interface
|
||||||
|
type Replication struct{}
|
||||||
|
|
||||||
|
// MaxFails returns that how many times this job can fail
|
||||||
|
func (r *Replication) MaxFails() uint {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRetry always returns true which means the job is needed to be restarted when fails
|
||||||
|
func (r *Replication) ShouldRetry() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate does nothing
|
||||||
|
func (r *Replication) Validate(params map[string]interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run gets the corresponding transfer according to the resource type
|
||||||
|
// and calls its function to do the real work
|
||||||
|
func (r *Replication) Run(ctx env.JobContext, params map[string]interface{}) error {
|
||||||
|
logger := ctx.GetLogger()
|
||||||
|
|
||||||
|
src, dst, err := parseParams(params)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to parse parameters: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
factory, err := transfer.GetFactory(src.Type)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to get transfer factory: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stopFunc := func() bool {
|
||||||
|
cmd, exist := ctx.OPCommand()
|
||||||
|
if !exist {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return cmd == opm.CtlCommandStop
|
||||||
|
}
|
||||||
|
transfer, err := factory(ctx.GetLogger(), stopFunc)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to create transfer: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transfer.Transfer(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseParams(params map[string]interface{}) (*model.Resource, *model.Resource, error) {
|
||||||
|
src := &model.Resource{}
|
||||||
|
if err := parseParam(params, "src_resource", src); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
dst := &model.Resource{}
|
||||||
|
if err := parseParam(params, "dst_resource", dst); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return src, dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseParam(params map[string]interface{}, name string, v interface{}) error {
|
||||||
|
value, exist := params[name]
|
||||||
|
if !exist {
|
||||||
|
return fmt.Errorf("param %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
str, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("the value of %s isn't string", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal([]byte(str), v)
|
||||||
|
}
|
100
src/jobservice/job/impl/replication/replication_test.go
Normal file
100
src/jobservice/job/impl/replication/replication_test.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// 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 replication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/job/impl"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/src/replication/transfer"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseParam(t *testing.T) {
|
||||||
|
params := map[string]interface{}{}
|
||||||
|
// not exist param
|
||||||
|
err := parseParam(params, "not_exist_param", nil)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
// the param is not string
|
||||||
|
params["num"] = 1
|
||||||
|
err = parseParam(params, "num", nil)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
// not a valid json struct
|
||||||
|
type person struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
params["person"] = `"name": "tom"`
|
||||||
|
p := &person{}
|
||||||
|
err = parseParam(params, "person", p)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
// pass
|
||||||
|
params["person"] = `{"name": "tom"}`
|
||||||
|
err = parseParam(params, "person", p)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "tom", p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseParams(t *testing.T) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"src_resource": `{"type":"chart"}`,
|
||||||
|
"dst_resource": `{"type":"chart"}`,
|
||||||
|
}
|
||||||
|
res, dst, err := parseParams(params)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "chart", string(res.Type))
|
||||||
|
assert.Equal(t, "chart", string(dst.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxFails(t *testing.T) {
|
||||||
|
rep := &Replication{}
|
||||||
|
assert.Equal(t, uint(3), rep.MaxFails())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldRetry(t *testing.T) {
|
||||||
|
rep := &Replication{}
|
||||||
|
assert.True(t, rep.ShouldRetry())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
rep := &Replication{}
|
||||||
|
assert.Nil(t, rep.Validate(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
var transferred = false
|
||||||
|
|
||||||
|
var fakedTransferFactory = func(transfer.Logger, transfer.StopFunc) (transfer.Transfer, error) {
|
||||||
|
return &fakedTransfer{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakedTransfer struct{}
|
||||||
|
|
||||||
|
func (f *fakedTransfer) Transfer(src *model.Resource, dst *model.Resource) error {
|
||||||
|
transferred = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
err := transfer.RegisterFactory("res", fakedTransferFactory)
|
||||||
|
require.Nil(t, err)
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"src_resource": `{"type":"res"}`,
|
||||||
|
"dst_resource": `{}`,
|
||||||
|
}
|
||||||
|
rep := &Replication{}
|
||||||
|
require.Nil(t, rep.Run(&impl.Context{}, params))
|
||||||
|
assert.True(t, transferred)
|
||||||
|
}
|
78
src/jobservice/job/impl/replication/scheduler.go
Normal file
78
src/jobservice/job/impl/replication/scheduler.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// 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 replication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
common_http "github.com/goharbor/harbor/src/common/http"
|
||||||
|
"github.com/goharbor/harbor/src/common/http/modifier/auth"
|
||||||
|
reg "github.com/goharbor/harbor/src/common/utils/registry"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/env"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/errs"
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/opm"
|
||||||
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scheduler is a job running in Jobservice which can be used as
|
||||||
|
// a scheduler when submitting it as a scheduled job. It receives
|
||||||
|
// a URL and data, and post the data to the URL when it is running
|
||||||
|
type Scheduler struct {
|
||||||
|
ctx env.JobContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRetry ...
|
||||||
|
func (s *Scheduler) ShouldRetry() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxFails ...
|
||||||
|
func (s *Scheduler) MaxFails() uint {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ....
|
||||||
|
func (s *Scheduler) Validate(params map[string]interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run ...
|
||||||
|
func (s *Scheduler) Run(ctx env.JobContext, params map[string]interface{}) error {
|
||||||
|
cmd, exist := ctx.OPCommand()
|
||||||
|
if exist && cmd == opm.CtlCommandStop {
|
||||||
|
return errs.JobStoppedError()
|
||||||
|
}
|
||||||
|
logger := ctx.GetLogger()
|
||||||
|
|
||||||
|
url := params["url"].(string)
|
||||||
|
url = fmt.Sprintf("%s/api/replication/executions?trigger=%s", url, model.TriggerTypeScheduled)
|
||||||
|
policyID := (int64)(params["policy_id"].(float64))
|
||||||
|
cred := auth.NewSecretAuthorizer(os.Getenv("JOBSERVICE_SECRET"))
|
||||||
|
client := common_http.NewClient(&http.Client{
|
||||||
|
Transport: reg.GetHTTPTransport(true),
|
||||||
|
}, cred)
|
||||||
|
if err := client.Post(url, struct {
|
||||||
|
PolicyID int64 `json:"policy_id"`
|
||||||
|
}{
|
||||||
|
PolicyID: policyID,
|
||||||
|
}); err != nil {
|
||||||
|
logger.Errorf("failed to run the schedule job: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("the schedule job finished")
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,368 +0,0 @@
|
|||||||
// 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 replication
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/distribution"
|
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
|
||||||
"github.com/docker/distribution/manifest/schema2"
|
|
||||||
common_http "github.com/goharbor/harbor/src/common/http"
|
|
||||||
"github.com/goharbor/harbor/src/common/http/modifier"
|
|
||||||
httpauth "github.com/goharbor/harbor/src/common/http/modifier/auth"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
|
||||||
reg "github.com/goharbor/harbor/src/common/utils/registry"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/env"
|
|
||||||
job_utils "github.com/goharbor/harbor/src/jobservice/job/impl/utils"
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errCanceled = errors.New("the job is canceled")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Transfer images from source registry to the destination one
|
|
||||||
type Transfer struct {
|
|
||||||
ctx env.JobContext
|
|
||||||
repository *repository
|
|
||||||
srcRegistry *registry
|
|
||||||
dstRegistry *registry
|
|
||||||
logger logger.Interface
|
|
||||||
retry bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldRetry : retry if the error is network error
|
|
||||||
func (t *Transfer) ShouldRetry() bool {
|
|
||||||
return t.retry
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxFails ...
|
|
||||||
func (t *Transfer) MaxFails() uint {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate ....
|
|
||||||
func (t *Transfer) Validate(params map[string]interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run ...
|
|
||||||
func (t *Transfer) Run(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
err := t.run(ctx, params)
|
|
||||||
t.retry = retry(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transfer) run(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
// initialize
|
|
||||||
if err := t.init(ctx, params); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// try to create project on destination registry
|
|
||||||
if err := t.createProject(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// replicate the images
|
|
||||||
for _, tag := range t.repository.tags {
|
|
||||||
digest, manifest, err := t.pullManifest(tag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.transferLayers(tag, manifest.References()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.pushManifest(tag, digest, manifest); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transfer) init(ctx env.JobContext, params map[string]interface{}) error {
|
|
||||||
t.logger = ctx.GetLogger()
|
|
||||||
t.ctx = ctx
|
|
||||||
|
|
||||||
if canceled(t.ctx) {
|
|
||||||
t.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
|
|
||||||
// init images that need to be replicated
|
|
||||||
t.repository = &repository{
|
|
||||||
name: params["repository"].(string),
|
|
||||||
}
|
|
||||||
if tags, ok := params["tags"]; ok {
|
|
||||||
tgs := tags.([]interface{})
|
|
||||||
for _, tg := range tgs {
|
|
||||||
t.repository.tags = append(t.repository.tags, tg.(string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
// init source registry client
|
|
||||||
srcURL := params["src_registry_url"].(string)
|
|
||||||
srcInsecure := params["src_registry_insecure"].(bool)
|
|
||||||
srcCred := httpauth.NewSecretAuthorizer(secret())
|
|
||||||
srcTokenServiceURL := ""
|
|
||||||
if stsu, ok := params["src_token_service_url"]; ok {
|
|
||||||
srcTokenServiceURL = stsu.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(srcTokenServiceURL) > 0 {
|
|
||||||
t.srcRegistry, err = initRegistry(srcURL, srcInsecure, srcCred, t.repository.name, srcTokenServiceURL)
|
|
||||||
} else {
|
|
||||||
t.srcRegistry, err = initRegistry(srcURL, srcInsecure, srcCred, t.repository.name)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("failed to create client for source registry: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init destination registry client
|
|
||||||
dstURL := params["dst_registry_url"].(string)
|
|
||||||
dstInsecure := params["dst_registry_insecure"].(bool)
|
|
||||||
dstCred := auth.NewBasicAuthCredential(
|
|
||||||
params["dst_registry_username"].(string),
|
|
||||||
params["dst_registry_password"].(string))
|
|
||||||
t.dstRegistry, err = initRegistry(dstURL, dstInsecure, dstCred, t.repository.name)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("failed to create client for destination registry: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the tag list first if it is null
|
|
||||||
if len(t.repository.tags) == 0 {
|
|
||||||
tags, err := t.srcRegistry.ListTag()
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while listing tags for the source repository: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tags) == 0 {
|
|
||||||
err = fmt.Errorf("empty tag list for repository %s", t.repository.name)
|
|
||||||
t.logger.Error(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.repository.tags = tags
|
|
||||||
}
|
|
||||||
|
|
||||||
t.logger.Infof("initialization completed: repository: %s, tags: %v, source registry: URL-%s insecure-%v, destination registry: URL-%s insecure-%v",
|
|
||||||
t.repository.name, t.repository.tags, t.srcRegistry.url, t.srcRegistry.insecure, t.dstRegistry.url, t.dstRegistry.insecure)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initRegistry(url string, insecure bool, credential modifier.Modifier,
|
|
||||||
repository string, tokenServiceURL ...string) (*registry, error) {
|
|
||||||
registry := ®istry{
|
|
||||||
url: url,
|
|
||||||
insecure: insecure,
|
|
||||||
}
|
|
||||||
|
|
||||||
// use the same transport for clients connecting to docker registry and Harbor UI
|
|
||||||
transport := reg.GetHTTPTransport(insecure)
|
|
||||||
|
|
||||||
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
}, credential, tokenServiceURL...)
|
|
||||||
uam := &job_utils.UserAgentModifier{
|
|
||||||
UserAgent: "harbor-registry-client",
|
|
||||||
}
|
|
||||||
repositoryClient, err := reg.NewRepository(repository, url,
|
|
||||||
&http.Client{
|
|
||||||
Transport: reg.NewTransport(transport, authorizer, uam),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
registry.Repository = *repositoryClient
|
|
||||||
|
|
||||||
registry.client = common_http.NewClient(
|
|
||||||
&http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
}, credential)
|
|
||||||
return registry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transfer) createProject() error {
|
|
||||||
if canceled(t.ctx) {
|
|
||||||
t.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
p, _ := utils.ParseRepository(t.repository.name)
|
|
||||||
project, err := t.srcRegistry.GetProject(p)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("failed to get project %s from source registry: %v", p, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = t.dstRegistry.CreateProject(project); err != nil {
|
|
||||||
// other jobs may be also doing the same thing when the current job
|
|
||||||
// is creating project or the project has already exist, so when the
|
|
||||||
// response code is 409, continue to do next step
|
|
||||||
if e, ok := err.(*common_http.Error); ok && e.Code == http.StatusConflict {
|
|
||||||
t.logger.Warningf("the status code is 409 when creating project %s on destination registry, try to do next step", p)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t.logger.Errorf("an error occurred while creating project %s on destination registry: %v", p, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.logger.Infof("project %s is created on destination registry", p)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transfer) pullManifest(tag string) (string, distribution.Manifest, error) {
|
|
||||||
if canceled(t.ctx) {
|
|
||||||
t.logger.Warning(errCanceled.Error())
|
|
||||||
return "", nil, errCanceled
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptMediaTypes := []string{schema1.MediaTypeManifest, schema2.MediaTypeManifest}
|
|
||||||
digest, mediaType, payload, err := t.srcRegistry.PullManifest(tag, acceptMediaTypes)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while pulling manifest of %s:%s from source registry: %v",
|
|
||||||
t.repository.name, tag, err)
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
t.logger.Infof("manifest of %s:%s pulled successfully from source registry: %s",
|
|
||||||
t.repository.name, tag, digest)
|
|
||||||
|
|
||||||
if strings.Contains(mediaType, "application/json") {
|
|
||||||
mediaType = schema1.MediaTypeManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest, _, err := reg.UnMarshal(mediaType, payload)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while parsing manifest: %v", err)
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return digest, manifest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transfer) transferLayers(tag string, blobs []distribution.Descriptor) error {
|
|
||||||
repository := t.repository.name
|
|
||||||
|
|
||||||
// all blobs(layers and config)
|
|
||||||
for _, blob := range blobs {
|
|
||||||
if canceled(t.ctx) {
|
|
||||||
t.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
|
|
||||||
digest := blob.Digest.String()
|
|
||||||
|
|
||||||
if blob.MediaType == schema2.MediaTypeForeignLayer {
|
|
||||||
t.logger.Infof("blob %s of %s:%s is an foreign layer, skip", digest, repository, tag)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
exist, err := t.dstRegistry.BlobExist(digest)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while checking existence of blob %s of %s:%s on destination registry: %v",
|
|
||||||
digest, repository, tag, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exist {
|
|
||||||
t.logger.Infof("blob %s of %s:%s already exists on the destination registry, skip",
|
|
||||||
digest, repository, tag)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.logger.Infof("transferring blob %s of %s:%s to the destination registry ...",
|
|
||||||
digest, repository, tag)
|
|
||||||
size, data, err := t.srcRegistry.PullBlob(digest)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while pulling blob %s of %s:%s from the source registry: %v",
|
|
||||||
digest, repository, tag, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if data != nil {
|
|
||||||
defer data.Close()
|
|
||||||
}
|
|
||||||
if err = t.dstRegistry.PushBlob(digest, size, data); err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while pushing blob %s of %s:%s to the distination registry: %v",
|
|
||||||
digest, repository, tag, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.logger.Infof("blob %s of %s:%s transferred to the destination registry completed",
|
|
||||||
digest, repository, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transfer) pushManifest(tag, digest string, manifest distribution.Manifest) error {
|
|
||||||
if canceled(t.ctx) {
|
|
||||||
t.logger.Warning(errCanceled.Error())
|
|
||||||
return errCanceled
|
|
||||||
}
|
|
||||||
|
|
||||||
repository := t.repository.name
|
|
||||||
dgt, exist, err := t.dstRegistry.ManifestExist(tag)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Warningf("an error occurred while checking the existence of manifest of %s:%s on the destination registry: %v, try to push manifest",
|
|
||||||
repository, tag, err)
|
|
||||||
} else {
|
|
||||||
if exist && dgt == digest {
|
|
||||||
t.logger.Infof("manifest of %s:%s exists on the destination registry, skip manifest pushing",
|
|
||||||
repository, tag)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaType, data, err := manifest.Payload()
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while getting payload of manifest for %s:%s : %v",
|
|
||||||
repository, tag, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = t.dstRegistry.PushManifest(tag, mediaType, data); err != nil {
|
|
||||||
t.logger.Errorf("an error occurred while pushing manifest of %s:%s to the destination registry: %v",
|
|
||||||
repository, tag, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.logger.Infof("manifest of %s:%s has been pushed to the destination registry",
|
|
||||||
repository, tag)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func canceled(ctx env.JobContext) bool {
|
|
||||||
_, canceled := ctx.OPCommand()
|
|
||||||
return canceled
|
|
||||||
}
|
|
||||||
|
|
||||||
func retry(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, ok := err.(net.Error)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func secret() string {
|
|
||||||
return os.Getenv("JOBSERVICE_SECRET")
|
|
||||||
}
|
|
@ -207,10 +207,9 @@ func (bs *Bootstrap) loadAndRunRedisWorkerPool(ctx *env.Context, cfg *config.Con
|
|||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
job.ImageScanJob: (*scan.ClairJob)(nil),
|
job.ImageScanJob: (*scan.ClairJob)(nil),
|
||||||
job.ImageScanAllJob: (*scan.All)(nil),
|
job.ImageScanAllJob: (*scan.All)(nil),
|
||||||
job.ImageTransfer: (*replication.Transfer)(nil),
|
|
||||||
job.ImageDelete: (*replication.Deleter)(nil),
|
|
||||||
job.ImageReplicate: (*replication.Replicator)(nil),
|
|
||||||
job.ImageGC: (*gc.GarbageCollector)(nil),
|
job.ImageGC: (*gc.GarbageCollector)(nil),
|
||||||
|
job.Replication: (*replication.Replication)(nil),
|
||||||
|
job.ReplicationScheduler: (*replication.Scheduler)(nil),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
// exit
|
// exit
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -258,9 +258,9 @@ export const DefaultServiceConfig: IServiceConfig = {
|
|||||||
systemInfoEndpoint: "/api/systeminfo",
|
systemInfoEndpoint: "/api/systeminfo",
|
||||||
repositoryBaseEndpoint: "/api/repositories",
|
repositoryBaseEndpoint: "/api/repositories",
|
||||||
logBaseEndpoint: "/api/logs",
|
logBaseEndpoint: "/api/logs",
|
||||||
targetBaseEndpoint: "/api/targets",
|
targetBaseEndpoint: "/api/registries",
|
||||||
replicationRuleEndpoint: "/api/policies/replication",
|
replicationRuleEndpoint: "/api/policies/replication",
|
||||||
replicationJobEndpoint: "/api/jobs/replication",
|
replicationBaseEndpoint: "/api/replication/executions",
|
||||||
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
||||||
configurationEndpoint: "/api/configurations",
|
configurationEndpoint: "/api/configurations",
|
||||||
enablei18Support: false,
|
enablei18Support: false,
|
||||||
@ -293,11 +293,11 @@ It supports partially overriding. For the items not overridden, default values w
|
|||||||
|
|
||||||
* **logBaseEndpoint:** The base endpoint of the service used to handle the recent access logs. Default is "/api/logs".
|
* **logBaseEndpoint:** The base endpoint of the service used to handle the recent access logs. Default is "/api/logs".
|
||||||
|
|
||||||
* **targetBaseEndpoint:** The base endpoint of the service used to handle the registry endpoints. Default is "/api/targets".
|
* **targetBaseEndpoint:** The base endpoint of the service used to handle the registry endpoints. Default is "/api/registries".
|
||||||
|
|
||||||
* **replicationRuleEndpoint:** The base endpoint of the service used to handle the replication rules. Default is "/api/policies/replication".
|
* **replicationRuleEndpoint:** The base endpoint of the service used to handle the replication rules. Default is "/api/policies/replication".
|
||||||
|
|
||||||
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
|
* **replicationBaseEndpoint:** The base endpoint of the service used to handle the replication executions. Default is "/api/replication/executions".
|
||||||
|
|
||||||
* **vulnerabilityScanningBaseEndpoint:** The base endpoint of the service used to handle the vulnerability scanning results.Default value is "/api/repositories".
|
* **vulnerabilityScanningBaseEndpoint:** The base endpoint of the service used to handle the vulnerability scanning results.Default value is "/api/repositories".
|
||||||
|
|
||||||
|
@ -26,5 +26,9 @@
|
|||||||
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="isDelete">{{'BUTTON.CANCEL' | translate}}</button>
|
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="isDelete">{{'BUTTON.CANCEL' | translate}}</button>
|
||||||
<button type="button" class="btn btn-primary" (click)="confirm()" [hidden]="isDelete">{{'BUTTON.REPLICATE' | translate}}</button>
|
<button type="button" class="btn btn-primary" (click)="confirm()" [hidden]="isDelete">{{'BUTTON.REPLICATE' | translate}}</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
<ng-template [ngSwitchCase]="5">
|
||||||
|
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="isDelete">{{'BUTTON.CANCEL' | translate}}</button>
|
||||||
|
<button type="button" class="btn btn-primary" (click)="confirm()" [hidden]="isDelete">{{'BUTTON.STOP' | translate}}</button>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</clr-modal>
|
</clr-modal>
|
@ -11,6 +11,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<form #targetForm="ngForm">
|
<form #targetForm="ngForm">
|
||||||
<section class="form-block">
|
<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 providerSelect pull-left">
|
||||||
|
<select name="adapter" id="adapter" [(ngModel)]="target.type" [disabled]="testOngoing || controlEnabled">
|
||||||
|
<option *ngFor="let adapter of adapterList" value="{{adapter}}">{{adapter}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Endpoint name -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="destination_name" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.NAME' |
|
<label for="destination_name" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.NAME' |
|
||||||
translate }}</label>
|
translate }}</label>
|
||||||
@ -23,30 +35,39 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label for="destination_url" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.URL' |
|
<label for="destination_url" class="col-md-4 form-group-label-override required">{{ 'DESTINATION.URL' |
|
||||||
translate }}</label>
|
translate }}</label>
|
||||||
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)"
|
<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">
|
[class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
|
||||||
<input type="text" id="destination_url" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.endpoint"
|
<input type="text" id="destination_url" [disabled]="testOngoing || controlEnabled" [readonly]="!editable" [(ngModel)]="target.url"
|
||||||
size="20" name="endpointUrl" #targetEndpoint="ngModel" required placeholder="http(s)://192.168.1.1">
|
size="20" name="endpointUrl" #targetEndpoint="ngModel" required placeholder="http(s)://192.168.1.1">
|
||||||
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
|
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
|
||||||
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
|
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- access_key -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="destination_username" class="col-md-4 form-group-label-override">{{ 'DESTINATION.USERNAME' |
|
<label for="destination_access_key" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_ID' |
|
||||||
translate }}</label>
|
translate }}</label>
|
||||||
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [readonly]="!editable"
|
<input type="text" placeholder="Access ID" class="col-md-8" id="destination_access_key" [disabled]="testOngoing" [readonly]="!editable"
|
||||||
[(ngModel)]="target.username" size="20" name="username" #username="ngModel">
|
[(ngModel)]="target.credential.access_key" size="23" name="access_key" #access_key="ngModel">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- access_secret -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.PASSWORD' |
|
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.ACCESS_SECRET' |
|
||||||
translate }}</label>
|
translate }}</label>
|
||||||
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
|
<input type="password" placeholder="Access Secret" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable"
|
||||||
[(ngModel)]="target.password" size="20" name="password" #password="ngModel">
|
[(ngModel)]="target.credential.access_secret" size="23" name="access_secret" #access_secret="ngModel">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Verify Remote Cert -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="destination_insecure" id="destination_insecure_checkbox">{{'CONFIG.VERIFY_REMOTE_CERT' |
|
<label for="destination_insecure" id="destination_insecure_checkbox">{{'CONFIG.VERIFY_REMOTE_CERT' |
|
||||||
translate }}</label>
|
translate }}</label>
|
||||||
|
@ -11,3 +11,11 @@
|
|||||||
.form-height {
|
.form-height {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.providerSelect {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputWidth {
|
||||||
|
width: 182px;
|
||||||
|
}
|
||||||
|
@ -22,14 +22,20 @@ import { of } from "rxjs";
|
|||||||
describe("CreateEditEndpointComponent (inline template)", () => {
|
describe("CreateEditEndpointComponent (inline template)", () => {
|
||||||
let mockData: Endpoint = {
|
let mockData: Endpoint = {
|
||||||
id: 1,
|
id: 1,
|
||||||
endpoint: "https://10.117.4.151",
|
credential: {
|
||||||
name: "target_01",
|
access_key: "admin",
|
||||||
username: "admin",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_01",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://10.117.4.151"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mockAdapters = ['harbor', 'docker hub'];
|
||||||
|
|
||||||
let comp: CreateEditEndpointComponent;
|
let comp: CreateEditEndpointComponent;
|
||||||
let fixture: ComponentFixture<CreateEditEndpointComponent>;
|
let fixture: ComponentFixture<CreateEditEndpointComponent>;
|
||||||
|
|
||||||
@ -40,6 +46,7 @@ describe("CreateEditEndpointComponent (inline template)", () => {
|
|||||||
let endpointService: EndpointService;
|
let endpointService: EndpointService;
|
||||||
|
|
||||||
let spy: jasmine.Spy;
|
let spy: jasmine.Spy;
|
||||||
|
let spyAdapter: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -62,6 +69,10 @@ describe("CreateEditEndpointComponent (inline template)", () => {
|
|||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
endpointService = fixture.debugElement.injector.get(EndpointService);
|
endpointService = fixture.debugElement.injector.get(EndpointService);
|
||||||
|
spyAdapter = spyOn(endpointService, "getAdapters").and.returnValue(
|
||||||
|
of(mockAdapters)
|
||||||
|
);
|
||||||
|
|
||||||
spy = spyOn(endpointService, "getEndpoint").and.returnValue(
|
spy = spyOn(endpointService, "getEndpoint").and.returnValue(
|
||||||
of(mockData)
|
of(mockData)
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,8 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
AfterViewChecked,
|
AfterViewChecked,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
OnDestroy
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { NgForm } from "@angular/forms";
|
import { NgForm } from "@angular/forms";
|
||||||
import { Subscription } from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
@ -30,7 +31,6 @@ import { InlineAlertComponent } from "../inline-alert/inline-alert.component";
|
|||||||
import { Endpoint } from "../service/interface";
|
import { Endpoint } from "../service/interface";
|
||||||
import { clone, compareValue, isEmptyObject } from "../utils";
|
import { clone, compareValue, isEmptyObject } from "../utils";
|
||||||
|
|
||||||
|
|
||||||
const FAKE_PASSWORD = "rjGcfuRu";
|
const FAKE_PASSWORD = "rjGcfuRu";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -39,16 +39,17 @@ const FAKE_PASSWORD = "rjGcfuRu";
|
|||||||
styleUrls: ["./create-edit-endpoint.component.scss"]
|
styleUrls: ["./create-edit-endpoint.component.scss"]
|
||||||
})
|
})
|
||||||
export class CreateEditEndpointComponent
|
export class CreateEditEndpointComponent
|
||||||
implements AfterViewChecked, OnDestroy {
|
implements AfterViewChecked, OnDestroy, OnInit {
|
||||||
modalTitle: string;
|
modalTitle: string;
|
||||||
|
controlEnabled: boolean = false;
|
||||||
createEditDestinationOpened: boolean;
|
createEditDestinationOpened: boolean;
|
||||||
staticBackdrop: boolean = true;
|
staticBackdrop: boolean = true;
|
||||||
closable: boolean = false;
|
closable: boolean = false;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
adapterList: string[];
|
||||||
target: Endpoint = this.initEndpoint();
|
target: Endpoint = this.initEndpoint();
|
||||||
|
selectedType: string;
|
||||||
initVal: Endpoint;
|
initVal: Endpoint;
|
||||||
|
|
||||||
targetForm: NgForm;
|
targetForm: NgForm;
|
||||||
@ViewChild("targetForm") currentForm: NgForm;
|
@ViewChild("targetForm") currentForm: NgForm;
|
||||||
|
|
||||||
@ -71,6 +72,17 @@ export class CreateEditEndpointComponent
|
|||||||
private ref: ChangeDetectorRef
|
private ref: ChangeDetectorRef
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.endpointService.getAdapters().subscribe(
|
||||||
|
adapters => {
|
||||||
|
this.adapterList = adapters || [];
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public get isValid(): boolean {
|
public get isValid(): boolean {
|
||||||
return (
|
return (
|
||||||
!this.testOngoing &&
|
!this.testOngoing &&
|
||||||
@ -98,12 +110,16 @@ export class CreateEditEndpointComponent
|
|||||||
|
|
||||||
initEndpoint(): Endpoint {
|
initEndpoint(): Endpoint {
|
||||||
return {
|
return {
|
||||||
endpoint: "",
|
credential: {
|
||||||
name: "",
|
access_key: "",
|
||||||
username: "",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "",
|
||||||
|
type: "harbor",
|
||||||
|
url: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +141,6 @@ export class CreateEditEndpointComponent
|
|||||||
this.initVal = this.initEndpoint();
|
this.initVal = this.initEndpoint();
|
||||||
this.formValues = null;
|
this.formValues = null;
|
||||||
this.endpointId = "";
|
this.endpointId = "";
|
||||||
|
|
||||||
this.inlineAlert.close();
|
this.inlineAlert.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,18 +168,21 @@ export class CreateEditEndpointComponent
|
|||||||
this.translateService
|
this.translateService
|
||||||
.get("DESTINATION.TITLE_EDIT")
|
.get("DESTINATION.TITLE_EDIT")
|
||||||
.subscribe(res => (this.modalTitle = res));
|
.subscribe(res => (this.modalTitle = res));
|
||||||
this.endpointService.getEndpoint(targetId)
|
this.endpointService.getEndpoint(targetId).subscribe(
|
||||||
.subscribe(target => {
|
target => {
|
||||||
this.target = target;
|
this.target = target;
|
||||||
// Keep data cache
|
// Keep data cache
|
||||||
this.initVal = clone(target);
|
this.initVal = clone(target);
|
||||||
this.initVal.password = FAKE_PASSWORD;
|
this.initVal.credential.access_secret = FAKE_PASSWORD;
|
||||||
this.target.password = FAKE_PASSWORD;
|
this.target.credential.access_secret = FAKE_PASSWORD;
|
||||||
|
|
||||||
// Open the modal now
|
// Open the modal now
|
||||||
this.open();
|
this.open();
|
||||||
|
this.controlEnabled = true;
|
||||||
this.forceRefreshView(2000);
|
this.forceRefreshView(2000);
|
||||||
}, error => this.errorHandler.error(error));
|
},
|
||||||
|
error => this.errorHandler.error(error)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.endpointId = "";
|
this.endpointId = "";
|
||||||
this.translateService
|
this.translateService
|
||||||
@ -172,15 +190,16 @@ export class CreateEditEndpointComponent
|
|||||||
.subscribe(res => (this.modalTitle = res));
|
.subscribe(res => (this.modalTitle = res));
|
||||||
// Directly open the modal
|
// Directly open the modal
|
||||||
this.open();
|
this.open();
|
||||||
|
this.controlEnabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testConnection() {
|
testConnection() {
|
||||||
let payload: Endpoint = this.initEndpoint();
|
let payload: Endpoint = this.initEndpoint();
|
||||||
if (!this.endpointId) {
|
if (!this.endpointId) {
|
||||||
payload.endpoint = this.target.endpoint;
|
payload.url = this.target.url;
|
||||||
payload.username = this.target.username;
|
payload.credential.access_key = this.target.credential.access_key;
|
||||||
payload.password = this.target.password;
|
payload.credential.access_secret = this.target.credential.access_secret;
|
||||||
payload.insecure = this.target.insecure;
|
payload.insecure = this.target.insecure;
|
||||||
} else {
|
} else {
|
||||||
let changes: { [key: string]: any } = this.getChanges();
|
let changes: { [key: string]: any } = this.getChanges();
|
||||||
@ -197,18 +216,20 @@ export class CreateEditEndpointComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.testOngoing = true;
|
this.testOngoing = true;
|
||||||
this.endpointService.pingEndpoint(payload)
|
this.endpointService.pingEndpoint(payload).subscribe(
|
||||||
.subscribe(response => {
|
response => {
|
||||||
this.inlineAlert.showInlineSuccess({
|
this.inlineAlert.showInlineSuccess({
|
||||||
message: "DESTINATION.TEST_CONNECTION_SUCCESS"
|
message: "DESTINATION.TEST_CONNECTION_SUCCESS"
|
||||||
});
|
});
|
||||||
this.forceRefreshView(2000);
|
this.forceRefreshView(2000);
|
||||||
this.testOngoing = false;
|
this.testOngoing = false;
|
||||||
}, error => {
|
},
|
||||||
|
error => {
|
||||||
this.inlineAlert.showInlineError("DESTINATION.TEST_CONNECTION_FAILURE");
|
this.inlineAlert.showInlineError("DESTINATION.TEST_CONNECTION_FAILURE");
|
||||||
this.forceRefreshView(2000);
|
this.forceRefreshView(2000);
|
||||||
this.testOngoing = false;
|
this.testOngoing = false;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
@ -223,10 +244,9 @@ export class CreateEditEndpointComponent
|
|||||||
if (this.onGoing) {
|
if (this.onGoing) {
|
||||||
return; // Avoid duplicated submitting
|
return; // Avoid duplicated submitting
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onGoing = true;
|
this.onGoing = true;
|
||||||
this.endpointService.createEndpoint(this.target)
|
this.endpointService.createEndpoint(this.target).subscribe(
|
||||||
.subscribe(response => {
|
response => {
|
||||||
this.translateService
|
this.translateService
|
||||||
.get("DESTINATION.CREATED_SUCCESS")
|
.get("DESTINATION.CREATED_SUCCESS")
|
||||||
.subscribe(res => this.errorHandler.info(res));
|
.subscribe(res => this.errorHandler.info(res));
|
||||||
@ -234,14 +254,13 @@ export class CreateEditEndpointComponent
|
|||||||
this.onGoing = false;
|
this.onGoing = false;
|
||||||
this.close();
|
this.close();
|
||||||
this.forceRefreshView(2000);
|
this.forceRefreshView(2000);
|
||||||
}, error => {
|
},
|
||||||
|
error => {
|
||||||
this.onGoing = false;
|
this.onGoing = false;
|
||||||
let errorMessageKey = this.handleErrorMessageKey(error.status);
|
this.inlineAlert.showInlineError(error);
|
||||||
this.translateService.get(errorMessageKey).subscribe(res => {
|
|
||||||
this.inlineAlert.showInlineError(res);
|
|
||||||
});
|
|
||||||
this.forceRefreshView(2000);
|
this.forceRefreshView(2000);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEndpoint() {
|
updateEndpoint() {
|
||||||
@ -257,6 +276,7 @@ export class CreateEditEndpointComponent
|
|||||||
if (isEmptyObject(changes)) {
|
if (isEmptyObject(changes)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let changekeys: { [key: string]: any } = Object.keys(changes);
|
let changekeys: { [key: string]: any } = Object.keys(changes);
|
||||||
|
|
||||||
changekeys.forEach((key: string) => {
|
changekeys.forEach((key: string) => {
|
||||||
@ -268,8 +288,8 @@ export class CreateEditEndpointComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.onGoing = true;
|
this.onGoing = true;
|
||||||
this.endpointService.updateEndpoint(this.target.id, payload)
|
this.endpointService.updateEndpoint(this.target.id, payload).subscribe(
|
||||||
.subscribe(response => {
|
response => {
|
||||||
this.translateService
|
this.translateService
|
||||||
.get("DESTINATION.UPDATED_SUCCESS")
|
.get("DESTINATION.UPDATED_SUCCESS")
|
||||||
.subscribe(res => this.errorHandler.info(res));
|
.subscribe(res => this.errorHandler.info(res));
|
||||||
@ -277,25 +297,13 @@ export class CreateEditEndpointComponent
|
|||||||
this.close();
|
this.close();
|
||||||
this.onGoing = false;
|
this.onGoing = false;
|
||||||
this.forceRefreshView(2000);
|
this.forceRefreshView(2000);
|
||||||
}, error => {
|
},
|
||||||
let errorMessageKey = this.handleErrorMessageKey(error.status);
|
error => {
|
||||||
this.translateService.get(errorMessageKey).subscribe(res => {
|
this.inlineAlert.showInlineError(error);
|
||||||
this.inlineAlert.showInlineError(res);
|
|
||||||
});
|
|
||||||
this.onGoing = false;
|
this.onGoing = false;
|
||||||
this.forceRefreshView(2000);
|
this.forceRefreshView(2000);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleErrorMessageKey(status: number): string {
|
|
||||||
switch (status) {
|
|
||||||
case 409:
|
|
||||||
return "DESTINATION.CONFLICT_NAME";
|
|
||||||
case 400:
|
|
||||||
return "DESTINATION.INVALID_NAME";
|
|
||||||
default:
|
|
||||||
return "UNKNOWN_ERROR";
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel() {
|
onCancel() {
|
||||||
@ -338,7 +346,6 @@ export class CreateEditEndpointComponent
|
|||||||
|
|
||||||
if (!compareValue(this.formValues, data)) {
|
if (!compareValue(this.formValues, data)) {
|
||||||
this.formValues = data;
|
this.formValues = data;
|
||||||
this.inlineAlert.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -353,6 +360,7 @@ export class CreateEditEndpointComponent
|
|||||||
}
|
}
|
||||||
for (let prop of Object.keys(this.target)) {
|
for (let prop of Object.keys(this.target)) {
|
||||||
let field: any = this.initVal[prop];
|
let field: any = this.initVal[prop];
|
||||||
|
if (typeof field !== "object") {
|
||||||
if (!compareValue(field, this.target[prop])) {
|
if (!compareValue(field, this.target[prop])) {
|
||||||
changes[prop] = this.target[prop];
|
changes[prop] = this.target[prop];
|
||||||
// Number
|
// Number
|
||||||
@ -365,8 +373,23 @@ export class CreateEditEndpointComponent
|
|||||||
changes[prop] = ("" + changes[prop]).trim();
|
changes[prop] = ("" + changes[prop]).trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (let pro of Object.keys(field)) {
|
||||||
|
if (!compareValue(field[pro], this.target[prop][pro])) {
|
||||||
|
changes[pro] = this.target[prop][pro];
|
||||||
|
// Number
|
||||||
|
if (typeof field[pro] === "number") {
|
||||||
|
changes[pro] = +changes[pro];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trim string value
|
||||||
|
if (typeof field[pro] === "string") {
|
||||||
|
changes[pro] = ("" + changes[pro]).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,23 +2,13 @@
|
|||||||
<h3 class="modal-title">{{headerTitle | translate}}</h3>
|
<h3 class="modal-title">{{headerTitle | translate}}</h3>
|
||||||
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
|
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
|
||||||
<div class="modal-body modal-body-height">
|
<div class="modal-body modal-body-height">
|
||||||
<clr-alert [hidden]='!deletedLabelCount' [clrAlertType]="'alert-warning'" [clrAlertSizeSmall]="true"
|
|
||||||
[clrAlertClosable]="false" [(clrAlertClosed)]="alertClosed">
|
|
||||||
<div class="alert-item">
|
|
||||||
<span class="alert-text">{{deletedLabelInfo}}</span>
|
|
||||||
<div class="alert-actions">
|
|
||||||
<a class="alert-action" (click)=" alertClosed = true">{{'REPLICATION.ACKNOWLEDGE' | translate}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</clr-alert>
|
|
||||||
<form [formGroup]="ruleForm" novalidate>
|
<form [formGroup]="ruleForm" novalidate>
|
||||||
<section class="form-block">
|
<section class="form-block">
|
||||||
<div class="form-group form-group-override">
|
<div class="form-group form-group-override">
|
||||||
<label class="form-group-label-override required">{{'REPLICATION.NAME' | translate}}</label>
|
<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"
|
<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'>
|
||||||
[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"
|
||||||
<input type="text" id="ruleName" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth" required
|
#ruleName (keyup)='checkRuleName()' autocomplete="off">
|
||||||
maxlength="255" formControlName="name" #ruleName (keyup)='checkRuleName()' autocomplete="off">
|
|
||||||
<span class="tooltip-content">{{ruleNameTooltip | translate}}</span>
|
<span class="tooltip-content">{{ruleNameTooltip | translate}}</span>
|
||||||
</label>
|
</label>
|
||||||
<span class="spinner spinner-inline spinner-pos" [hidden]="!inNameChecking"></span>
|
<span class="spinner spinner-inline spinner-pos" [hidden]="!inNameChecking"></span>
|
||||||
@ -28,127 +18,119 @@
|
|||||||
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
|
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
|
||||||
<textarea type="text" id="ruleDescription" class="inputWidth" row=3 formControlName="description"></textarea>
|
<textarea type="text" id="ruleDescription" class="inputWidth" row=3 formControlName="description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<!--Projects-->
|
<!-- replication mode -->
|
||||||
<div class="form-group form-group-override">
|
<div class="form-group form-group-override">
|
||||||
<label class="form-group-label-override required">{{'REPLICATION.SOURCE_PROJECT' | translate}}</label>
|
<label class="form-group-label-override">{{'REPLICATION.REPLI_MODE' | translate}}</label>
|
||||||
<div formArrayName="projects">
|
<div class="radio-inline" [class.disabled]="policyId >= 0">
|
||||||
<div class="projectInput inputWidth" *ngFor="let project of projects.controls; let i= index"
|
<input type="radio" id="push_base" name="replicationMode" [value]=true [disabled]="policyId >= 0" [(ngModel)]="isPushMode" (change)="modeChange()" [ngModelOptions]="{standalone: true}">
|
||||||
[formGroupName]="i" (mouseleave)="leaveInput()">
|
<label for="push_base">Push-based</label>
|
||||||
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
|
</div>
|
||||||
[class.invalid]='noProjectInfo'>
|
<div class="radio-inline" [class.disabled]="policyId >= 0">
|
||||||
<input *ngIf="!projectId" formControlName="name" type="text" class="inputWidth" value="name" required
|
<input type="radio" id="pull_base" name="replicationMode" [value]=false [disabled]="policyId >= 0" [(ngModel)]="isPushMode" [ngModelOptions]="{standalone: true}">
|
||||||
pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" (keyup)='handleValidation()' (focus)="focusClear($event)"
|
<label for="pull_base">Pull-based</label>
|
||||||
autocomplete="off">
|
|
||||||
<input *ngIf="projectId" formControlName="name" type="text" class="inputWidth" value="name" readonly>
|
|
||||||
<span class="tooltip-content">{{noProjectInfo | translate}}</span>
|
|
||||||
</label>
|
|
||||||
<div class="selectBox inputWidth" [style.display]="selectedProjectList.length ? 'block' : 'none'">
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let project of selectedProjectList" (click)="selectedProjectName(project?.name)">{{project?.name}}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!--source registry-->
|
||||||
</div>
|
<div *ngIf="!isPushMode" class="form-group form-group-override">
|
||||||
|
<label class="form-group-label-override required">{{'REPLICATION.SOURCE_REGISTRY' | translate}}</label>
|
||||||
<!--images/Filter-->
|
<div class="form-select">
|
||||||
<div class="form-group form-group-override">
|
<div class="select endpointSelect pull-left">
|
||||||
<label class="form-group-label-override">{{'REPLICATION.SOURCE_IMAGES_FILTER' | translate}}</label>
|
<select id="src_registry_id" (change)="sourceChange($event)" formControlName="src_registry" [compareWith]="equals">
|
||||||
<div formArrayName="filters">
|
<option *ngFor="let source of sourceList" [ngValue]="source">{{source.name}}-{{source.url}}</option>
|
||||||
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
|
|
||||||
<div [formGroupName]="i">
|
|
||||||
<div class="select floatSetPar">
|
|
||||||
<select formControlName="kind" #selectedValue (change)="filterChange($event, selectedValue.value)" id="{{i}}"
|
|
||||||
name="{{filterListData[i]?.name}}">
|
|
||||||
<option *ngFor="let opt of filterListData[i]?.options;" value="{{opt}}">{{opt}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<label 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 type="text" #filterValue required size="14" formControlName="value" [attr.disabled]="(filterListData[i]?.name=='label') ?'' : null">
|
|
||||||
<span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
|
|
||||||
</label>
|
|
||||||
<div class="arrowSet" [hidden]="!(filterListData[i]?.name=='label')" (click)="openLabelList(selectedValue.value, i, $event)">
|
|
||||||
<clr-icon shape="angle" ></clr-icon>
|
|
||||||
</div>
|
|
||||||
<clr-icon shape="warning-standard" class="is-solid is-warning warning-icon" size="14" [hidden]="!deletedLabelCount || !(filterListData[i]?.name=='label')"></clr-icon>
|
|
||||||
<clr-icon shape="times-circle" class="is-solid" (click)="deleteFilter(i)"></clr-icon>
|
|
||||||
<div *ngIf="!withAdmiral">
|
|
||||||
<hbr-filter-label [projectId]="projectId" [selectedLabelInfo]="filterLabelInfo" [isOpen]="filterListData[i].isOpen"
|
|
||||||
(selectedLabels)="selectedLabelList($event, i)" (closePanelEvent)="filterListData[i].isOpen = false"></hbr-filter-label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<clr-icon id="add-label-list" shape="plus-circle" class="is-solid plus-position" [hidden]="isFilterHide" (click)="addNewFilter()"></clr-icon>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!--Targets-->
|
|
||||||
<div class="form-group form-group-override">
|
|
||||||
<label class="form-group-label-override required">{{'DESTINATION.ENDPOINT' | translate}}</label>
|
|
||||||
<div formArrayName="targets" class="form-select">
|
|
||||||
<div class="select endpointSelect pull-left" *ngFor="let target of targets.controls; let i= index"
|
|
||||||
[formGroupName]="i">
|
|
||||||
<select id="ruleTarget" (change)="targetChange($event)" formControlName="id">
|
|
||||||
<option *ngFor="let target of targetList" value="{{target.id}}">{{target.name}}-{{target.endpoint}}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
|
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
|
||||||
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS'
|
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
|
||||||
| translate}}</span>
|
</div>
|
||||||
|
<!--images/Filter-->
|
||||||
|
<div class="form-group form-group-override">
|
||||||
|
<label class="form-group-label-override">{{'REPLICATION.SOURCE_RESOURCE_FILTER' | translate}}</label>
|
||||||
|
<div formArrayName="filters">
|
||||||
|
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
|
||||||
|
<div [formGroupName]="i">
|
||||||
|
<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 type="text" #filterValue required size="14" formControlName="value">
|
||||||
|
<span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
|
||||||
|
</label>
|
||||||
|
<div class="select inline-block" *ngIf="supportedFilters[i]?.style==='radio'">
|
||||||
|
<select formControlName="value" #selectedValue id="{{i}}" name="{{supportedFilters[i]?.type}}">
|
||||||
|
<option value=""></option>
|
||||||
|
<option *ngFor="let value of supportedFilters[i]?.values;" value="{{value}}">{{value}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 *ngFor="let target of targetList" [ngValue]="target">{{target.name}}-{{target.url}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
|
||||||
|
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
|
||||||
|
</div>
|
||||||
|
<!--destination namespaces -->
|
||||||
|
<div class="form-group form-group-override">
|
||||||
|
<label class="form-group-label-override">{{'REPLICATION.DEST_NAMESPACE' | translate}}</label>
|
||||||
|
<div class="form-select">
|
||||||
|
<div class="endpointSelect pull-left">
|
||||||
|
<input formControlName="dest_namespace" type="text" id="dest_namespace" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth"
|
||||||
|
maxlength="255">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Trigger-->
|
<!--Trigger-->
|
||||||
<div class="form-group form-group-override">
|
<div class="form-group form-group-override">
|
||||||
<label class="form-group-label-override">{{'REPLICATION.TRIGGER_MODE' | translate}}</label>
|
<label class="form-group-label-override required">{{'REPLICATION.TRIGGER_MODE' | translate}}</label>
|
||||||
<div formGroupName="trigger">
|
<div formGroupName="trigger">
|
||||||
<!--on trigger-->
|
<!--on trigger-->
|
||||||
<div class="select floatSetPar">
|
<div class="select width-115">
|
||||||
<select id="ruleTrigger" formControlName="kind" (change)="selectTrigger($event)">
|
<select id="ruleTrigger" formControlName="type">
|
||||||
<option value="Manual">{{'REPLICATION.MANUAL' | translate}}</option>
|
<option *ngFor="let trigger of supportedTriggers" [value]="trigger">{{'REPLICATION.' + trigger.toUpperCase() | translate }}</option>
|
||||||
<option value="Immediate">{{'REPLICATION.IMMEDIATE' | translate}}</option>
|
|
||||||
<option value="Scheduled">{{'REPLICATION.SCHEDULE' | translate}}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<!--on push-->
|
<!--on push-->
|
||||||
<div formGroupName="schedule_param" class="schedule-style">
|
<div formGroupName="trigger_settings">
|
||||||
<div class="select floatSet" [hidden]="!isScheduleOpt">
|
<div [hidden]="isNotSchedule()" class="form-group form-cron">
|
||||||
<select name="scheduleType" formControlName="type" (change)="selectSchedule($event)">
|
<label class="required">Cron String</label>
|
||||||
<option value="Daily">{{'REPLICATION.DAILY' | translate}}</option>
|
<label for="targetCron" aria-haspopup="true" role="tooltip"class="tooltip tooltip-validation tooltip-md tooltip-top-right"
|
||||||
<option value="Weekly">{{'REPLICATION.WEEKLY' | translate}}</option>
|
[class.invalid]="!isNotSchedule() && cronTouched || !cronInputValid(ruleForm.value.trigger?.trigger_settings?.cron || '')" >
|
||||||
</select>
|
<input type="text" name=targetCron id="targetCron" required class="form-control cron-input" formControlName="cron">
|
||||||
</div>
|
<span class="tooltip-content">
|
||||||
<!--weekly-->
|
{{'TOOLTIP.CRON_REQUIRED' | translate }}
|
||||||
<span [hidden]="!weeklySchedule || !isScheduleOpt">on </span>
|
</span>
|
||||||
<div [hidden]="!weeklySchedule || !isScheduleOpt" class="select floatSet weekday-width">
|
</label>
|
||||||
<select name="scheduleDay" formControlName="weekday">
|
|
||||||
<option value="1">{{'WEEKLY.MONDAY' | translate}}</option>
|
|
||||||
<option value="2">{{'WEEKLY.TUESDAY' | translate}}</option>
|
|
||||||
<option value="3">{{'WEEKLY.WEDNESDAY' | translate}}</option>
|
|
||||||
<option value="4">{{'WEEKLY.THURSDAY' | translate}}</option>
|
|
||||||
<option value="5">{{'WEEKLY.FRIDAY' | translate}}</option>
|
|
||||||
<option value="6">{{'WEEKLY.SATURDAY' | translate}}</option>
|
|
||||||
<option value="7">{{'WEEKLY.SUNDAY' | translate}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<!--daily/time-->
|
|
||||||
<span [hidden]="!isScheduleOpt">at </span>
|
|
||||||
<input [hidden]="!isScheduleOpt" type="time" formControlName="offtime" required value="08:00" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="!isImmediate" class="clr-form-control rule-width">
|
</div>
|
||||||
|
<div [hidden]="isNotEventBased()" class="clr-form-control rule-width">
|
||||||
<clr-checkbox-wrapper>
|
<clr-checkbox-wrapper>
|
||||||
<input type="checkbox" clrCheckbox [checked]="false" id="ruleDeletion" formControlName="replicate_deletion" class="clr-checkbox">
|
<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>
|
<label for="ruleDeletion" class="clr-control-label">{{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}}</label>
|
||||||
</clr-checkbox-wrapper>
|
</clr-checkbox-wrapper>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-form-control rule-width">
|
<div class="rule-width">
|
||||||
<clr-checkbox-wrapper>
|
<clr-checkbox-wrapper>
|
||||||
<input type="checkbox" clrCheckbox [checked]="true" id="ruleExit" formControlName="replicate_existing_image_now"
|
<input type="checkbox" clrCheckbox [checked]="true" id="enablePolicy" formControlName="enabled" class="clr-checkbox">
|
||||||
class="clr-checkbox">
|
<label for="enablePolicy" class="clr-control-label">{{'REPLICATION.ENABLED' | translate}}</label>
|
||||||
<label for="ruleExit" class="clr-control-label">{{'REPLICATION.REPLICATE_IMMEDIATE' | translate}}</label>
|
</clr-checkbox-wrapper>
|
||||||
|
</div>
|
||||||
|
<div class="rule-width">
|
||||||
|
<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' | translate}}</label>
|
||||||
</clr-checkbox-wrapper>
|
</clr-checkbox-wrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -159,9 +141,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="this.inProgress" (click)="onCancel()">{{
|
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="this.inProgress" (click)="onCancel()">{{ 'BUTTON.CANCEL' | translate }}</button>
|
||||||
'BUTTON.CANCEL' | translate }}</button>
|
<button type="submit" id="ruleBtnOk" class="btn btn-primary" (click)="onSubmit()" [disabled]="!isValid || !hasFormChange()">{{ 'BUTTON.SAVE' | translate }}</button>
|
||||||
<button type="submit" id="ruleBtnOk" class="btn btn-primary" (click)="onSubmit()" [disabled]="!ruleForm.valid || !isValid || !hasFormChange()">{{
|
|
||||||
'BUTTON.SAVE' | translate }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</clr-modal>
|
</clr-modal>
|
@ -1,5 +1,5 @@
|
|||||||
.select {
|
.select {
|
||||||
width: 186px;
|
width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select .optionMore {
|
.select .optionMore {
|
||||||
@ -40,7 +40,7 @@ h4 {
|
|||||||
|
|
||||||
.endpointSelect {
|
.endpointSelect {
|
||||||
width: 270px;
|
width: 270px;
|
||||||
margin-right: 20px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterSelect {
|
.filterSelect {
|
||||||
@ -49,11 +49,11 @@ h4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filterSelect clr-icon {
|
.filterSelect clr-icon {
|
||||||
margin-left: 15px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterSelect label {
|
.filterSelect label {
|
||||||
width: 136px;
|
width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterSelect label input {
|
.filterSelect label input {
|
||||||
@ -68,9 +68,15 @@ h4 {
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floatSetPar {
|
.width-70 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 120px;
|
width: 70px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-115 {
|
||||||
|
display: inline-block;
|
||||||
|
width: 115px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +152,19 @@ h4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-group-override {
|
.form-group-override {
|
||||||
padding-left: 170px !important;
|
padding-left: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group>label:first-child {
|
.form-group {
|
||||||
|
>label:first-child{
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 6.5rem;
|
width: 7rem;
|
||||||
|
}
|
||||||
|
.radio {
|
||||||
|
label {
|
||||||
|
margin-right:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-select {
|
.form-select {
|
||||||
@ -199,7 +212,7 @@ clr-modal {
|
|||||||
margin-left: -15px;
|
margin-left: -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plus-position {
|
.mr-t-11{
|
||||||
margin-top: 11px;
|
margin-top: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,3 +228,21 @@ clr-modal {
|
|||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.width-315 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width:315px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-t-10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.form-cron {
|
||||||
|
padding-left:3.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -32,14 +32,12 @@ import {
|
|||||||
EndpointService,
|
EndpointService,
|
||||||
EndpointDefaultService
|
EndpointDefaultService
|
||||||
} from "../service/endpoint.service";
|
} from "../service/endpoint.service";
|
||||||
import {
|
|
||||||
ProjectDefaultService,
|
|
||||||
ProjectService
|
|
||||||
} from "../service/project.service";
|
|
||||||
import { OperationService } from "../operation/operation.service";
|
import { OperationService } from "../operation/operation.service";
|
||||||
import {FilterLabelComponent} from "./filter-label.component";
|
import {FilterLabelComponent} from "./filter-label.component";
|
||||||
import {LabelService} from "../service/label.service";
|
import {LabelService} from "../service/label.service";
|
||||||
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
|
|
||||||
describe("CreateEditRuleComponent (inline template)", () => {
|
describe("CreateEditRuleComponent (inline template)", () => {
|
||||||
@ -48,74 +46,51 @@ describe("CreateEditRuleComponent (inline template)", () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: "sync_01",
|
name: "sync_01",
|
||||||
description: "",
|
description: "",
|
||||||
projects: [
|
src_registry: {id: 2},
|
||||||
{
|
src_namespaces: ["name1", "name2"],
|
||||||
project_id: 1,
|
|
||||||
owner_id: 0,
|
|
||||||
name: "project_01",
|
|
||||||
creation_time: "",
|
|
||||||
deleted: 0,
|
|
||||||
owner_name: "",
|
|
||||||
togglable: false,
|
|
||||||
update_time: "",
|
|
||||||
current_user_role_id: 0,
|
|
||||||
repo_count: 0,
|
|
||||||
has_project_admin_role: false,
|
|
||||||
is_member: false,
|
|
||||||
role_name: "",
|
|
||||||
metadata: {
|
|
||||||
public: "",
|
|
||||||
enable_content_trust: "",
|
|
||||||
prevent_vul: "",
|
|
||||||
severity: "",
|
|
||||||
auto_scan: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
endpoint: "https://10.117.4.151",
|
|
||||||
name: "target_01",
|
|
||||||
username: "admin",
|
|
||||||
password: "",
|
|
||||||
insecure: false,
|
|
||||||
type: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
trigger: {
|
trigger: {
|
||||||
kind: "Manual",
|
type: "Manual",
|
||||||
schedule_param: null
|
trigger_settings: {}
|
||||||
},
|
},
|
||||||
filters: [],
|
filters: [],
|
||||||
replicate_existing_image_now: false,
|
deletion: false,
|
||||||
replicate_deletion: false
|
enabled: true,
|
||||||
|
override: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
let mockJobs: ReplicationJobItem[] = [
|
let mockJobs: ReplicationJobItem[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
status: "stopped",
|
status: "stopped",
|
||||||
repository: "library/busybox",
|
|
||||||
policy_id: 1,
|
policy_id: 1,
|
||||||
operation: "transfer",
|
trigger: "Manual",
|
||||||
tags: null
|
total: 0,
|
||||||
|
failed: 0,
|
||||||
|
succeed: 0,
|
||||||
|
in_progress: 0,
|
||||||
|
stopped: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
status: "stopped",
|
status: "stopped",
|
||||||
repository: "library/busybox",
|
|
||||||
policy_id: 1,
|
policy_id: 1,
|
||||||
operation: "transfer",
|
trigger: "Manual",
|
||||||
tags: null
|
total: 1,
|
||||||
|
failed: 0,
|
||||||
|
succeed: 1,
|
||||||
|
in_progress: 0,
|
||||||
|
stopped: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
status: "stopped",
|
status: "stopped",
|
||||||
repository: "library/busybox",
|
|
||||||
policy_id: 2,
|
policy_id: 2,
|
||||||
operation: "transfer",
|
trigger: "Manual",
|
||||||
tags: null
|
total: 1,
|
||||||
|
failed: 1,
|
||||||
|
succeed: 0,
|
||||||
|
in_progress: 0,
|
||||||
|
stopped: 0
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -127,39 +102,55 @@ describe("CreateEditRuleComponent (inline template)", () => {
|
|||||||
let mockEndpoints: Endpoint[] = [
|
let mockEndpoints: Endpoint[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
endpoint: "https://10.117.4.151",
|
credential: {
|
||||||
name: "target_01",
|
access_key: "admin",
|
||||||
username: "admin",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_01",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://10.117.4.151"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
endpoint: "https://10.117.5.142",
|
credential: {
|
||||||
name: "target_02",
|
access_key: "AAA",
|
||||||
username: "AAA",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_02",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://10.117.5.142"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
endpoint: "https://101.1.11.111",
|
credential: {
|
||||||
name: "target_03",
|
access_key: "admin",
|
||||||
username: "admin",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_03",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://101.1.11.111"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
endpoint: "http://4.4.4.4",
|
credential: {
|
||||||
name: "target_04",
|
access_key: "admin",
|
||||||
username: "",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: true,
|
insecure: true,
|
||||||
type: 0
|
name: "target_04",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://4.4.4.4"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -167,48 +158,49 @@ describe("CreateEditRuleComponent (inline template)", () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: "sync_01",
|
name: "sync_01",
|
||||||
description: "",
|
description: "",
|
||||||
projects: [
|
src_namespaces: ["namespace1", "namespace2"],
|
||||||
{
|
src_registry: {id: 10 },
|
||||||
project_id: 1,
|
dest_registry: {id: 0 },
|
||||||
owner_id: 0,
|
|
||||||
name: "project_01",
|
|
||||||
creation_time: "",
|
|
||||||
deleted: 0,
|
|
||||||
owner_name: "",
|
|
||||||
togglable: false,
|
|
||||||
update_time: "",
|
|
||||||
current_user_role_id: 0,
|
|
||||||
repo_count: 0,
|
|
||||||
has_project_admin_role: false,
|
|
||||||
is_member: false,
|
|
||||||
role_name: "",
|
|
||||||
metadata: {
|
|
||||||
public: "",
|
|
||||||
enable_content_trust: "",
|
|
||||||
prevent_vul: "",
|
|
||||||
severity: "",
|
|
||||||
auto_scan: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
endpoint: "https://10.117.4.151",
|
|
||||||
name: "target_01",
|
|
||||||
username: "admin",
|
|
||||||
password: "",
|
|
||||||
insecure: false,
|
|
||||||
type: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
trigger: {
|
trigger: {
|
||||||
kind: "Manual",
|
type: "Manual",
|
||||||
schedule_param: null
|
trigger_settings: {}
|
||||||
},
|
},
|
||||||
filters: [],
|
filters: [],
|
||||||
replicate_existing_image_now: false,
|
deletion: false,
|
||||||
replicate_deletion: false
|
enabled: true,
|
||||||
|
override: true
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockRegistryInfo = {
|
||||||
|
"type": "harbor",
|
||||||
|
"description": "",
|
||||||
|
"supported_resource_filters": [
|
||||||
|
{
|
||||||
|
"type": "Name",
|
||||||
|
"style": "input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Version",
|
||||||
|
"style": "input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Label",
|
||||||
|
"style": "input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"style": "radio",
|
||||||
|
"values": [
|
||||||
|
"repository",
|
||||||
|
"chart"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported_triggers": [
|
||||||
|
"manual",
|
||||||
|
"scheduled",
|
||||||
|
"event_based"
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
let fixture: ComponentFixture<ReplicationComponent>;
|
let fixture: ComponentFixture<ReplicationComponent>;
|
||||||
@ -224,16 +216,18 @@ describe("CreateEditRuleComponent (inline template)", () => {
|
|||||||
let spyOneRule: jasmine.Spy;
|
let spyOneRule: jasmine.Spy;
|
||||||
|
|
||||||
let spyJobs: jasmine.Spy;
|
let spyJobs: jasmine.Spy;
|
||||||
|
let spyAdapter: jasmine.Spy;
|
||||||
let spyEndpoint: jasmine.Spy;
|
let spyEndpoint: jasmine.Spy;
|
||||||
|
|
||||||
|
|
||||||
let config: IServiceConfig = {
|
let config: IServiceConfig = {
|
||||||
replicationJobEndpoint: "/api/jobs/replication/testing",
|
replicationBaseEndpoint: "/api/replication/testing",
|
||||||
targetBaseEndpoint: "/api/targets/testing"
|
targetBaseEndpoint: "/api/registries/testing"
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [SharedModule, NoopAnimationsModule],
|
imports: [SharedModule, NoopAnimationsModule, RouterTestingModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
ReplicationComponent,
|
ReplicationComponent,
|
||||||
ListReplicationRuleComponent,
|
ListReplicationRuleComponent,
|
||||||
@ -250,7 +244,6 @@ describe("CreateEditRuleComponent (inline template)", () => {
|
|||||||
{ provide: SERVICE_CONFIG, useValue: config },
|
{ provide: SERVICE_CONFIG, useValue: config },
|
||||||
{ provide: ReplicationService, useClass: ReplicationDefaultService },
|
{ provide: ReplicationService, useClass: ReplicationDefaultService },
|
||||||
{ provide: EndpointService, useClass: EndpointDefaultService },
|
{ provide: EndpointService, useClass: EndpointDefaultService },
|
||||||
{ provide: ProjectService, useClass: ProjectDefaultService },
|
|
||||||
{ provide: JobLogService, useClass: JobLogDefaultService },
|
{ provide: JobLogService, useClass: JobLogDefaultService },
|
||||||
{ provide: OperationService },
|
{ provide: OperationService },
|
||||||
{ provide: LabelService }
|
{ provide: LabelService }
|
||||||
@ -278,10 +271,11 @@ describe("CreateEditRuleComponent (inline template)", () => {
|
|||||||
replicationService,
|
replicationService,
|
||||||
"getReplicationRule"
|
"getReplicationRule"
|
||||||
).and.returnValue(of(mockRule));
|
).and.returnValue(of(mockRule));
|
||||||
spyJobs = spyOn(replicationService, "getJobs").and.returnValues(
|
spyJobs = spyOn(replicationService, "getExecutions").and.returnValues(
|
||||||
of(mockJob)
|
of(mockJob));
|
||||||
);
|
|
||||||
|
|
||||||
|
spyAdapter = spyOn(replicationService, "getRegistryInfo").and.returnValues(
|
||||||
|
of(mockRegistryInfo));
|
||||||
spyEndpoint = spyOn(endpointService, "getEndpoints").and.returnValues(
|
spyEndpoint = spyOn(endpointService, "getEndpoints").and.returnValues(
|
||||||
of(mockEndpoints)
|
of(mockEndpoints)
|
||||||
);
|
);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -17,17 +17,27 @@
|
|||||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length ===1)" (click)="editTargets(selectedRow)" ><clr-icon shape="pencil" size="16"></clr-icon> {{'DESTINATION.EDIT' | translate}}</button>
|
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length ===1)" (click)="editTargets(selectedRow)" ><clr-icon shape="pencil" size="16"></clr-icon> {{'DESTINATION.EDIT' | translate}}</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow.length" (click)="deleteTargets(selectedRow)"><clr-icon shape="times" size="16"></clr-icon> {{'DESTINATION.DELETE' | translate}}</button>
|
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow.length" (click)="deleteTargets(selectedRow)"><clr-icon shape="times" size="16"></clr-icon> {{'DESTINATION.DELETE' | translate}}</button>
|
||||||
</clr-dg-action-bar>
|
</clr-dg-action-bar>
|
||||||
<clr-dg-column [clrDgField]="'name'">{{'DESTINATION.NAME' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgField]="'name'" class="flex-min-width">{{'DESTINATION.NAME' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'endpoint'">{{'DESTINATION.URL' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgField]="'status'" class="flex-min-width">{{'DESTINATION.STATUS' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'url'" class="flex-min-width">{{'DESTINATION.URL' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'type'">{{'DESTINATION.PROVIDER' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'insecure'">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</clr-dg-column>
|
<clr-dg-column [clrDgField]="'insecure'">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'credential.type'">{{'DESTINATION.AUTHENTICATION' | translate }}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||||
<clr-dg-placeholder>{{'DESTINATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
<clr-dg-placeholder>{{'DESTINATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||||
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
|
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
|
||||||
<clr-dg-cell>{{t.name}}</clr-dg-cell>
|
<clr-dg-cell class="flex-min-width">{{t.name}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
|
<clr-dg-cell [ngSwitch]="t.status" class="flex-min-width">
|
||||||
|
<div *ngSwitchCase="'unhealthy'"><clr-icon shape="exclamation-circle" class="is-error text-alignment" size="22"></clr-icon> Unhealthy</div>
|
||||||
|
<div *ngSwitchCase="'healthy'"><clr-icon shape="success-standard" class="is-success text-alignment" size="18"></clr-icon> Healthy</div>
|
||||||
|
<div *ngSwitchCase="'unknown' || ''"><clr-icon shape="exclamation-triangle" class="is-warning text-alignment" size="22"></clr-icon> Unknown</div>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell class="flex-min-width">{{t.url}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.type}}</clr-dg-cell>
|
||||||
<clr-dg-cell>
|
<clr-dg-cell>
|
||||||
{{!t.insecure}}
|
{{!t.insecure}}
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.credential.type}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell>
|
<clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell>
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
|
@ -26,3 +26,7 @@
|
|||||||
.endpoint-view {
|
.endpoint-view {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-min-width {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
@ -25,54 +25,76 @@ describe("EndpointComponent (inline template)", () => {
|
|||||||
let mockData: Endpoint[] = [
|
let mockData: Endpoint[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
endpoint: "https://10.117.4.151",
|
credential: {
|
||||||
|
access_key: "admin",
|
||||||
|
access_secret: "",
|
||||||
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
|
insecure: false,
|
||||||
name: "target_01",
|
name: "target_01",
|
||||||
username: "admin",
|
type: "Harbor",
|
||||||
password: "",
|
url: "https://10.117.4.151"
|
||||||
insecure: true,
|
|
||||||
type: 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
endpoint: "https://10.117.5.142",
|
credential: {
|
||||||
name: "target_02",
|
access_key: "AAA",
|
||||||
username: "AAA",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_02",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://10.117.5.142"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
endpoint: "https://101.1.11.111",
|
credential: {
|
||||||
name: "target_03",
|
access_key: "admin",
|
||||||
username: "admin",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_03",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://101.1.11.111"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
endpoint: "http://4.4.4.4",
|
credential: {
|
||||||
name: "target_04",
|
access_key: "admin",
|
||||||
username: "",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_04",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://4.4.4.4"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
let mockOne: Endpoint[] = [
|
let mockOne: Endpoint[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
endpoint: "https://10.117.4.151",
|
credential: {
|
||||||
name: "target_01",
|
access_key: "admin",
|
||||||
username: "admin",
|
access_secret: "",
|
||||||
password: "",
|
type: "basic"
|
||||||
|
},
|
||||||
|
description: "test",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "target_01",
|
||||||
|
type: "Harbor",
|
||||||
|
url: "https://10.117.4.151"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let mockAdapters = ['harbor', 'docker hub'];
|
||||||
|
|
||||||
let comp: EndpointComponent;
|
let comp: EndpointComponent;
|
||||||
let fixture: ComponentFixture<EndpointComponent>;
|
let fixture: ComponentFixture<EndpointComponent>;
|
||||||
let config: IServiceConfig = {
|
let config: IServiceConfig = {
|
||||||
@ -81,6 +103,7 @@ describe("EndpointComponent (inline template)", () => {
|
|||||||
|
|
||||||
let endpointService: EndpointService;
|
let endpointService: EndpointService;
|
||||||
let spy: jasmine.Spy;
|
let spy: jasmine.Spy;
|
||||||
|
let spyAdapter: jasmine.Spy;
|
||||||
let spyOnRules: jasmine.Spy;
|
let spyOnRules: jasmine.Spy;
|
||||||
let spyOne: jasmine.Spy;
|
let spyOne: jasmine.Spy;
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@ -111,6 +134,11 @@ describe("EndpointComponent (inline template)", () => {
|
|||||||
spy = spyOn(endpointService, "getEndpoints").and.returnValues(
|
spy = spyOn(endpointService, "getEndpoints").and.returnValues(
|
||||||
of(mockData)
|
of(mockData)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
spyAdapter = spyOn(endpointService, "getAdapters").and.returnValue(
|
||||||
|
of(mockAdapters)
|
||||||
|
);
|
||||||
|
|
||||||
spyOnRules = spyOn(
|
spyOnRules = spyOn(
|
||||||
endpointService,
|
endpointService,
|
||||||
"getEndpointWithReplicationRules"
|
"getEndpointWithReplicationRules"
|
||||||
|
@ -76,12 +76,16 @@ export class EndpointComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
get initEndpoint(): Endpoint {
|
get initEndpoint(): Endpoint {
|
||||||
return {
|
return {
|
||||||
endpoint: "",
|
credential: {
|
||||||
name: "",
|
access_key: "",
|
||||||
username: "",
|
access_secret: "",
|
||||||
password: "",
|
type: ""
|
||||||
|
},
|
||||||
|
description: "",
|
||||||
insecure: false,
|
insecure: false,
|
||||||
type: 0
|
name: "",
|
||||||
|
type: "",
|
||||||
|
url: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +94,6 @@ export class EndpointComponent implements OnInit, OnDestroy {
|
|||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private operationService: OperationService,
|
private operationService: OperationService,
|
||||||
private ref: ChangeDetectorRef) {
|
private ref: ChangeDetectorRef) {
|
||||||
this.forceRefreshView(1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -114,8 +117,8 @@ export class EndpointComponent implements OnInit, OnDestroy {
|
|||||||
this.endpointService.getEndpoints(this.targetName)
|
this.endpointService.getEndpoints(this.targetName)
|
||||||
.subscribe(targets => {
|
.subscribe(targets => {
|
||||||
this.targets = targets || [];
|
this.targets = targets || [];
|
||||||
this.forceRefreshView(1000);
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.forceRefreshView(1000);
|
||||||
}, error => {
|
}, error => {
|
||||||
this.errorHandler.error(error);
|
this.errorHandler.error(error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
@ -79,13 +79,13 @@ import { OperationService } from './operation/operation.service';
|
|||||||
* this default configuration.
|
* this default configuration.
|
||||||
*/
|
*/
|
||||||
export const DefaultServiceConfig: IServiceConfig = {
|
export const DefaultServiceConfig: IServiceConfig = {
|
||||||
|
baseEndpoint: "/api",
|
||||||
systemInfoEndpoint: "/api/systeminfo",
|
systemInfoEndpoint: "/api/systeminfo",
|
||||||
repositoryBaseEndpoint: "/api/repositories",
|
repositoryBaseEndpoint: "/api/repositories",
|
||||||
logBaseEndpoint: "/api/logs",
|
logBaseEndpoint: "/api/logs",
|
||||||
targetBaseEndpoint: "/api/targets",
|
targetBaseEndpoint: "/api/registries",
|
||||||
replicationBaseEndpoint: "/api/replications",
|
replicationBaseEndpoint: "/api/replication",
|
||||||
replicationRuleEndpoint: "/api/policies/replication",
|
replicationRuleEndpoint: "/api/replication/policies",
|
||||||
replicationJobEndpoint: "/api/jobs/replication",
|
|
||||||
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
||||||
projectPolicyEndpoint: "/api/projects/configs",
|
projectPolicyEndpoint: "/api/projects/configs",
|
||||||
projectBaseEndpoint: "/api/projects",
|
projectBaseEndpoint: "/api/projects",
|
||||||
|
@ -6,28 +6,41 @@
|
|||||||
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasDeleteReplicationPermission" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon> {{'REPLICATION.DELETE_POLICY' | translate}}</button>
|
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasDeleteReplicationPermission" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon> {{'REPLICATION.DELETE_POLICY' | translate}}</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasExecuteReplicationPermission" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)"><clr-icon shape="export" size="16"></clr-icon> {{'REPLICATION.REPLICATE' | translate}}</button>
|
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasExecuteReplicationPermission" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)"><clr-icon shape="export" size="16"></clr-icon> {{'REPLICATION.REPLICATE' | translate}}</button>
|
||||||
</clr-dg-action-bar>
|
</clr-dg-action-bar>
|
||||||
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgField]="'status'" class="status-width">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
|
<clr-dg-column class="col-width">{{'REPLICATION.SRC_REGISTRY' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column class="col-width">{{'REPLICATION.REPLICATION_MODE' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column class="min-width">{{'REPLICATION.DESTINATION_NAMESPACE' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'trigger'">{{'REPLICATION.REPLICATION_TRIGGER' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'override'" class="status-width">{{'REPLICATION.OVERRIDE' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column >{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
|
|
||||||
<clr-dg-column [clrDgField]="'trigger'">{{'REPLICATION.TRIGGER_MODE' | translate}}</clr-dg-column>
|
|
||||||
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||||
<clr-dg-row *clrDgItems="let p of changedRules" [clrDgItem]="p" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
|
<clr-dg-row *clrDgItems="let p of changedRules; let i=index" [clrDgItem]="p" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
|
||||||
<clr-dg-cell>{{p.name}}</clr-dg-cell>
|
<clr-dg-cell>{{p.name}}</clr-dg-cell>
|
||||||
<clr-dg-cell>
|
<clr-dg-cell class="status-width">
|
||||||
<div [ngSwitch]="hasDeletedLabel(p)">
|
<div [ngSwitch]="p.enabled">
|
||||||
<clr-tooltip *ngSwitchCase="'disabled'" class="tooltip-lg">
|
<clr-tooltip *ngSwitchCase="false" class="tooltip-lg">
|
||||||
<clr-icon clrTooltipTrigger shape="exclamation-triangle" class="is-warning text-alignment" size="22"></clr-icon>Disabled
|
<clr-icon clrTooltipTrigger shape="exclamation-triangle" class="is-warning text-alignment" size="22"></clr-icon>Disabled
|
||||||
<clr-tooltip-content clrPosition="top-right" clrSize="xs" *clrIfOpen>
|
<clr-tooltip-content clrPosition="top-right" clrSize="xs" *clrIfOpen>
|
||||||
<span>{{'REPLICATION.RULE_DISABLED' | translate}}</span>
|
<span>{{'REPLICATION.RULE_DISABLED' | translate}}</span>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
<div *ngSwitchCase="'enabled'" ><clr-icon shape="success-standard" class="is-success text-alignment" size="18"></clr-icon> Enabled</div>
|
<div *ngSwitchCase="true" ><clr-icon shape="success-standard" class="is-success text-alignment" size="18"></clr-icon> Enabled</div>
|
||||||
</div>
|
</div>
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
<clr-dg-cell *ngIf="!projectScope">
|
<clr-dg-cell class="col-width">
|
||||||
<a href="javascript:void(0)" (click)="$event.stopPropagation(); redirectTo(p)">{{p.projects?.length>0 ? p.projects[0].name : ''}}</a>
|
{{p.src_registry ? p.src_registry.name : ''}}
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell class="col-width">
|
||||||
|
{{p.src_registry && p.src_registry.id > 0 ? 'pull-based' : 'push-based'}}
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell class="min-width">
|
||||||
|
{{p.dest_registry ? p.dest_registry.name : ''}} : {{p.dest_namespace? p.dest_namespace: '-'}}
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{p.trigger ? p.trigger.type : ''}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell [ngSwitch]="p.override">
|
||||||
|
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>
|
||||||
|
<clr-icon shape="times-circle" *ngSwitchCase="false" size="16" class="icon-style"></clr-icon>
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
<clr-dg-cell>
|
<clr-dg-cell>
|
||||||
{{p.description ? trancatedDescription(p.description) : '-'}}
|
{{p.description ? trancatedDescription(p.description) : '-'}}
|
||||||
@ -38,8 +51,6 @@
|
|||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
<clr-dg-cell>{{p.targets?.length>0 ? p.targets[0].name : ''}}</clr-dg-cell>
|
|
||||||
<clr-dg-cell>{{p.trigger ? p.trigger.kind : ''}}</clr-dg-cell>
|
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'REPLICATION.OF' | translate}} </span>{{pagination.totalItems }} {{'REPLICATION.ITEMS' | translate}}
|
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'REPLICATION.OF' | translate}} </span>{{pagination.totalItems }} {{'REPLICATION.ITEMS' | translate}}
|
||||||
|
@ -5,3 +5,16 @@
|
|||||||
.text-alignment {
|
.text-alignment {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
.min-width {
|
||||||
|
width: 224px;
|
||||||
|
}
|
||||||
|
.col-width {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
.status-width {
|
||||||
|
width: 105px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-style {
|
||||||
|
color: #C92100;
|
||||||
|
}
|
||||||
|
@ -21,83 +21,29 @@ describe('ListReplicationRuleComponent (inline template)', () => {
|
|||||||
let mockRules: ReplicationRule[] = [
|
let mockRules: ReplicationRule[] = [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"projects": [{
|
|
||||||
"project_id": 33,
|
|
||||||
"owner_id": 1,
|
|
||||||
"name": "aeas",
|
|
||||||
"deleted": 0,
|
|
||||||
"togglable": false,
|
|
||||||
"current_user_role_id": 0,
|
|
||||||
"repo_count": 0,
|
|
||||||
"metadata": {
|
|
||||||
"public": false,
|
|
||||||
"enable_content_trust": "",
|
|
||||||
"prevent_vul": "",
|
|
||||||
"severity": "",
|
|
||||||
"auto_scan": ""},
|
|
||||||
"owner_name": "",
|
|
||||||
"creation_time": null,
|
|
||||||
"update_time": null,
|
|
||||||
"has_project_admin_role": true,
|
|
||||||
"is_member": true,
|
|
||||||
"role_name": ""
|
|
||||||
}],
|
|
||||||
"targets": [{
|
|
||||||
"endpoint": "",
|
|
||||||
"id": 0,
|
|
||||||
"insecure": false,
|
|
||||||
"name": "khans3",
|
|
||||||
"username": "",
|
|
||||||
"password": "",
|
|
||||||
"type": 0,
|
|
||||||
}],
|
|
||||||
"name": "sync_01",
|
"name": "sync_01",
|
||||||
"description": "",
|
"description": "",
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
"trigger": {"type": "Manual", "trigger_settings": null},
|
||||||
"error_job_count": 2,
|
"error_job_count": 2,
|
||||||
"replicate_deletion": false,
|
"deletion": false,
|
||||||
"replicate_existing_image_now": false,
|
"src_namespaces": ["name1", "name2"],
|
||||||
|
"src_registry": {id: 3},
|
||||||
|
"enabled": true,
|
||||||
|
"override": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"projects": [{
|
|
||||||
"project_id": 33,
|
|
||||||
"owner_id": 1,
|
|
||||||
"name": "aeas",
|
|
||||||
"deleted": 0,
|
|
||||||
"togglable": false,
|
|
||||||
"current_user_role_id": 0,
|
|
||||||
"repo_count": 0,
|
|
||||||
"metadata": {
|
|
||||||
"public": false,
|
|
||||||
"enable_content_trust": "",
|
|
||||||
"prevent_vul": "",
|
|
||||||
"severity": "",
|
|
||||||
"auto_scan": ""},
|
|
||||||
"owner_name": "",
|
|
||||||
"creation_time": null,
|
|
||||||
"update_time": null,
|
|
||||||
"has_project_admin_role": true,
|
|
||||||
"is_member": true,
|
|
||||||
"role_name": ""
|
|
||||||
}],
|
|
||||||
"targets": [{
|
|
||||||
"endpoint": "",
|
|
||||||
"id": 0,
|
|
||||||
"insecure": false,
|
|
||||||
"name": "khans3",
|
|
||||||
"username": "",
|
|
||||||
"password": "",
|
|
||||||
"type": 0,
|
|
||||||
}],
|
|
||||||
"name": "sync_02",
|
"name": "sync_02",
|
||||||
"description": "",
|
"description": "",
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
"trigger": {"type": "Manual", "trigger_settings": null},
|
||||||
"error_job_count": 2,
|
"error_job_count": 2,
|
||||||
"replicate_deletion": false,
|
"deletion": false,
|
||||||
"replicate_existing_image_now": false,
|
"src_namespaces": ["name1", "name2"],
|
||||||
|
"dest_registry": {id: 3},
|
||||||
|
"enabled": true,
|
||||||
|
"override": true
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -137,6 +83,7 @@ describe('ListReplicationRuleComponent (inline template)', () => {
|
|||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
replicationService = fixture.debugElement.injector.get(ReplicationService);
|
replicationService = fixture.debugElement.injector.get(ReplicationService);
|
||||||
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(of(mockRules));
|
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(of(mockRules));
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -27,9 +27,8 @@ import {
|
|||||||
import { Comparator } from "../service/interface";
|
import { Comparator } from "../service/interface";
|
||||||
import { TranslateService } from "@ngx-translate/core";
|
import { TranslateService } from "@ngx-translate/core";
|
||||||
import { map, catchError } from "rxjs/operators";
|
import { map, catchError } from "rxjs/operators";
|
||||||
import { Observable, forkJoin, throwError as observableThrowError } from "rxjs";
|
import { Observable, forkJoin, of, throwError as observableThrowError } from "rxjs";
|
||||||
import { ReplicationService } from "../service/replication.service";
|
import { ReplicationService } from "../service/replication.service";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ReplicationJob,
|
ReplicationJob,
|
||||||
ReplicationJobItem,
|
ReplicationJobItem,
|
||||||
@ -198,7 +197,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
|||||||
let ruleData: ReplicationJobItem[];
|
let ruleData: ReplicationJobItem[];
|
||||||
this.canDeleteRule = true;
|
this.canDeleteRule = true;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
return this.replicationService.getJobs(id)
|
|
||||||
|
return this.replicationService.getExecutions(id)
|
||||||
.pipe(map(response => {
|
.pipe(map(response => {
|
||||||
ruleData = response.data;
|
ruleData = response.data;
|
||||||
if (ruleData.length) {
|
if (ruleData.length) {
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Type } from '@angular/core';
|
import { Type } from '@angular/core';
|
||||||
import { ReplicationComponent } from './replication.component';
|
import { ReplicationComponent } from './replication.component';
|
||||||
|
import { ReplicationTasksComponent } from './replication-tasks/replication-tasks.component';
|
||||||
|
|
||||||
export * from './replication.component';
|
export * from './replication.component';
|
||||||
|
export * from './replication-tasks/replication-tasks.component';
|
||||||
|
|
||||||
export const REPLICATION_DIRECTIVES: Type<any>[] = [
|
export const REPLICATION_DIRECTIVES: Type<any>[] = [
|
||||||
ReplicationComponent
|
ReplicationComponent,
|
||||||
|
ReplicationTasksComponent
|
||||||
];
|
];
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
<div class="replication-tasks">
|
||||||
|
<section class="overview-section">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<a (click)="onBack()" class="onback"><{{'PROJECT_DETAIL.REPLICATION'|
|
||||||
|
translate}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="title-block">
|
||||||
|
<div>
|
||||||
|
<h2 class="custom-h2 h2-style">{{'REPLICATION.REPLICATION_EXECUTION'|
|
||||||
|
translate}}</h2>
|
||||||
|
<span class="id-divider"></span>
|
||||||
|
<h2 class="custom-h2 h2-style">{{executionId}}</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="status-progress" *ngIf="executions && executions['status'] === 'InProgress'">
|
||||||
|
<span class="spinner spinner-inline"></span>
|
||||||
|
<span>{{'REPLICATION.IN_PROGRESS'| translate}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-success" *ngIf="executions && executions['status'] === 'Succeed'">
|
||||||
|
<clr-icon size="18" shape="success-standard" class="color-green"></clr-icon>
|
||||||
|
<span>{{'REPLICATION.SUCCESS'| translate}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-failed" *ngIf="executions && executions['status'] === 'Failed'">
|
||||||
|
<clr-icon size="18" shape="error-standard" class="color-red"></clr-icon>
|
||||||
|
<span>{{'REPLICATION.FAILURE'| translate}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary btn-sm" (click)="stopJob()"
|
||||||
|
[disabled]="stopOnGoing">{{'REPLICATION.STOPJOB' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="execution-block">
|
||||||
|
<div class="executions-detail">
|
||||||
|
<div>
|
||||||
|
<label>{{'REPLICATION.TRIGGER_MODE' | translate}} :</label>
|
||||||
|
<span>{{trigger| translate}}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>{{'REPLICATION.CREATION_TIME' | translate}} :</label>
|
||||||
|
<span>{{startTime | date: 'short'}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-block">
|
||||||
|
<section class="execution-detail-label">
|
||||||
|
<section class="detail-row">
|
||||||
|
<div class="num-success common-style"></div>
|
||||||
|
<label class="detail-span">{{'REPLICATION.SUCCESS'| translate}}</label>
|
||||||
|
<div class="execution-details">{{successNum}}</div>
|
||||||
|
</section>
|
||||||
|
<section class="detail-row">
|
||||||
|
<div class="num-failed common-style"></div>
|
||||||
|
<label class="detail-span">{{'REPLICATION.FAILURE'| translate}}</label>
|
||||||
|
<div class="execution-details">{{failedNum}}</div>
|
||||||
|
</section>
|
||||||
|
<section class="detail-row">
|
||||||
|
<div class="num-progress common-style"></div>
|
||||||
|
<label class="detail-span">{{'REPLICATION.IN_PROGRESS'| translate}}</label>
|
||||||
|
<div class="execution-details">{{progressNum}}</div>
|
||||||
|
</section>
|
||||||
|
<section class="detail-row">
|
||||||
|
<div class="num-stopped common-style"></div>
|
||||||
|
<label class="detail-span">{{'REPLICATION.STOPPED'| translate}}</label>
|
||||||
|
<div class="execution-details">{{stoppedNum}}</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="tasks-detail">
|
||||||
|
<h3 class="modal-title">Tasks</h3>
|
||||||
|
<div class="row flex-items-xs-between flex-items-xs-bottom">
|
||||||
|
<div class="action-select">
|
||||||
|
<div class="select filter-tag" [hidden]="!isOpenFilterTag">
|
||||||
|
<select (change)="selectFilter($event)">
|
||||||
|
<option value="resource_type">{{'REPLICATION.RESOURCE_TYPE' |translate}}</option>
|
||||||
|
<option value="status">{{'REPLICATION.STATUS' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<hbr-filter [withDivider]="true" (openFlag)="openFilter($event)"
|
||||||
|
filterPlaceholder='{{"REPLICATION.FILTER_PLACEHOLDER" | translate}}'
|
||||||
|
(filterEvt)="doSearch($event)" [currentValue]="searchTask"></hbr-filter>
|
||||||
|
<span class="refresh-btn">
|
||||||
|
<clr-icon shape="refresh" (click)="refreshTasks()"></clr-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<clr-datagrid (clrDgRefresh)="clrLoadTasks($event)" [(clrDgSelected)]="selectedRow" [clrDgLoading]="loading">
|
||||||
|
<clr-dg-column>{{'REPLICATION.TASK_ID'| translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'resource_type'">{{'REPLICATION.RESOURCE_TYPE'
|
||||||
|
| translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'src_resource'">{{'REPLICATION.SOURCE' |
|
||||||
|
translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'dst_resource'">{{'REPLICATION.DESTINATION' |
|
||||||
|
translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'operation'">{{'REPLICATION.OPERATION' |
|
||||||
|
translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' |
|
||||||
|
translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.CREATION_TIME'
|
||||||
|
| translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgSortBy]="endTimeComparator">{{'REPLICATION.END_TIME'
|
||||||
|
| translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-row *clrDgItems="let t of tasks">
|
||||||
|
<clr-dg-cell>{{t.id}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.resource_type}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.src_resource}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.dst_resource}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.operation}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.status}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.start_time | date: 'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{t.end_time | date: 'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span *ngIf="t.status=='InProgress'; else elseBlock" class="label">{{'REPLICATION.NO_LOGS'
|
||||||
|
| translate}}</span>
|
||||||
|
<ng-template #elseBlock>
|
||||||
|
<a target="_blank" [href]="viewLog(t.id)">
|
||||||
|
<clr-icon shape="list"></clr-icon>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
<clr-dg-footer>
|
||||||
|
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}}
|
||||||
|
-
|
||||||
|
{{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' |
|
||||||
|
translate}} </span>
|
||||||
|
{{pagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
|
||||||
|
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,110 @@
|
|||||||
|
.replication-tasks {
|
||||||
|
.overview-section {
|
||||||
|
.title-wrapper {
|
||||||
|
.onback{
|
||||||
|
color: #007cbb;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.title-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
>div:first-child {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
>div:nth-child(2) {
|
||||||
|
width: 140px;
|
||||||
|
span {
|
||||||
|
color: #007cbb;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.id-divider {
|
||||||
|
display: inline-block;
|
||||||
|
height: 25px;
|
||||||
|
width: 2px;
|
||||||
|
background-color: #cccccc;
|
||||||
|
margin: 0 20px;
|
||||||
|
}
|
||||||
|
.h2-style {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.execution-block {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: row wrap;
|
||||||
|
.execution-detail-label {
|
||||||
|
margin-right: 10px;
|
||||||
|
text-align: left;
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
height: 27px;
|
||||||
|
.common-style {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.num-success {
|
||||||
|
background-color: #308700;
|
||||||
|
}
|
||||||
|
.num-failed {
|
||||||
|
background-color: #C92101;
|
||||||
|
}
|
||||||
|
.num-progress {
|
||||||
|
background-color: #1C5898;
|
||||||
|
}
|
||||||
|
.num-stopped {
|
||||||
|
background-color: #A1A1A1;
|
||||||
|
}
|
||||||
|
.detail-span {
|
||||||
|
flex:0 0 100px;
|
||||||
|
margin: 10px 0 0 10px;
|
||||||
|
}
|
||||||
|
.execution-details {
|
||||||
|
width: 200px;
|
||||||
|
margin: 8px 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.executions-detail {
|
||||||
|
width: 400px;
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
color: black;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
>div {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tasks-detail {
|
||||||
|
margin-top: 65px;
|
||||||
|
.action-select {
|
||||||
|
padding-right: 18px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.filter-tag {
|
||||||
|
float: left;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.refresh-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
color: #007CBB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clr-datagrid {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,243 @@
|
|||||||
|
import { Component, OnInit, Input, OnDestroy } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ReplicationService } from "../../service/replication.service";
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { finalize } from "rxjs/operators";
|
||||||
|
import { Subscription, timer } from "rxjs";
|
||||||
|
import { ErrorHandler } from "../../error-handler/error-handler";
|
||||||
|
import { ReplicationJob, ReplicationTasks, Comparator, ReplicationJobItem, State } from "../../service/interface";
|
||||||
|
import { CustomComparator, DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting } from "../../utils";
|
||||||
|
import { RequestQueryParams } from "../../service/RequestQueryParams";
|
||||||
|
const taskStatus: any = {
|
||||||
|
PENDING: "pending",
|
||||||
|
RUNNING: "running",
|
||||||
|
SCHEDULED: "scheduled"
|
||||||
|
};
|
||||||
|
@Component({
|
||||||
|
selector: 'replication-tasks',
|
||||||
|
templateUrl: './replication-tasks.component.html',
|
||||||
|
styleUrls: ['./replication-tasks.component.scss']
|
||||||
|
})
|
||||||
|
export class ReplicationTasksComponent implements OnInit, OnDestroy {
|
||||||
|
isOpenFilterTag: boolean;
|
||||||
|
selectedRow: [];
|
||||||
|
currentPage: number = 1;
|
||||||
|
currentPagePvt: number = 0;
|
||||||
|
totalCount: number = 0;
|
||||||
|
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||||
|
currentState: State;
|
||||||
|
loading = true;
|
||||||
|
searchTask: string;
|
||||||
|
defaultFilter = "resource_type";
|
||||||
|
tasks: ReplicationTasks;
|
||||||
|
taskItem: ReplicationTasks[] = [];
|
||||||
|
tasksCopy: ReplicationTasks[] = [];
|
||||||
|
stopOnGoing: boolean;
|
||||||
|
executions: ReplicationJobItem[];
|
||||||
|
timerDelay: Subscription;
|
||||||
|
@Input() executionId: string;
|
||||||
|
startTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
|
||||||
|
ReplicationJob
|
||||||
|
>("start_time", "date");
|
||||||
|
endTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
|
||||||
|
ReplicationJob
|
||||||
|
>("end_time", "date");
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private translate: TranslateService,
|
||||||
|
private router: Router,
|
||||||
|
private replicationService: ReplicationService,
|
||||||
|
private errorHandler: ErrorHandler,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.searchTask = '';
|
||||||
|
this.getExecutionDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
getExecutionDetail(): void {
|
||||||
|
if (this.executionId) {
|
||||||
|
this.replicationService.getExecutionById(this.executionId)
|
||||||
|
.subscribe(res => {
|
||||||
|
this.executions = res.data;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get trigger(): string {
|
||||||
|
return this.executions && this.executions['trigger']
|
||||||
|
? this.executions['trigger']
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get startTime(): Date {
|
||||||
|
return this.executions && this.executions['start_time']
|
||||||
|
? this.executions['start_time']
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get successNum(): string {
|
||||||
|
return this.executions && this.executions['succeed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get failedNum(): string {
|
||||||
|
return this.executions && this.executions['failed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get progressNum(): string {
|
||||||
|
return this.executions && this.executions['in_progress'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get stoppedNum(): string {
|
||||||
|
return this.executions && this.executions['stopped'];
|
||||||
|
}
|
||||||
|
|
||||||
|
stopJob() {
|
||||||
|
this.stopOnGoing = true;
|
||||||
|
this.replicationService.stopJobs(this.executionId)
|
||||||
|
.subscribe(response => {
|
||||||
|
this.stopOnGoing = false;
|
||||||
|
this.getExecutionDetail();
|
||||||
|
this.translate.get("REPLICATION.STOP_SUCCESS", { param: this.executionId }).subscribe((res: string) => {
|
||||||
|
this.errorHandler.info(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLog(taskId: number | string): string {
|
||||||
|
return this.replicationService.getJobBaseUrl() + "/executions/" + this.executionId + "/tasks/" + taskId + "/log";
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.timerDelay) {
|
||||||
|
this.timerDelay.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clrLoadTasks(state: State): void {
|
||||||
|
if (!state || !state.page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Keep it for future filter
|
||||||
|
this.currentState = state;
|
||||||
|
|
||||||
|
let pageNumber: number = calculatePage(state);
|
||||||
|
if (pageNumber !== this.currentPagePvt) {
|
||||||
|
// load data
|
||||||
|
let params: RequestQueryParams = new RequestQueryParams();
|
||||||
|
params.set("page", '' + pageNumber);
|
||||||
|
params.set("page_size", '' + this.pageSize);
|
||||||
|
if (this.searchTask && this.searchTask !== "") {
|
||||||
|
params.set(this.defaultFilter, this.searchTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.replicationService.getReplicationTasks(this.executionId, params)
|
||||||
|
.pipe(finalize(() => (this.loading = false)))
|
||||||
|
.subscribe(res => {
|
||||||
|
this.totalCount = res.length;
|
||||||
|
this.tasks = res; // Keep the data
|
||||||
|
this.taskItem = this.tasks.filter(tasks => tasks.resource_type !== "");
|
||||||
|
if (!this.timerDelay) {
|
||||||
|
this.timerDelay = timer(10000, 10000).subscribe(() => {
|
||||||
|
let count: number = 0;
|
||||||
|
this.tasks.forEach(tasks => {
|
||||||
|
if (
|
||||||
|
tasks.status.toLowerCase() === taskStatus.PENDING ||
|
||||||
|
tasks.status.toLowerCase() === taskStatus.RUNNING ||
|
||||||
|
tasks.status.toLowerCase() === taskStatus.SCHEDULED
|
||||||
|
) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (count > 0) {
|
||||||
|
this.clrLoadTasks(this.currentState);
|
||||||
|
} else {
|
||||||
|
this.timerDelay.unsubscribe();
|
||||||
|
this.timerDelay = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.taskItem = doFiltering<ReplicationTasks>(this.taskItem, state);
|
||||||
|
|
||||||
|
this.taskItem = doSorting<ReplicationTasks>(this.taskItem, state);
|
||||||
|
|
||||||
|
this.currentPagePvt = pageNumber;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
this.taskItem = this.tasks.filter(tasks => tasks.resource_type !== "");
|
||||||
|
// Do customized filter
|
||||||
|
this.taskItem = doFiltering<ReplicationTasks>(this.taskItem, state);
|
||||||
|
|
||||||
|
// Do customized sorting
|
||||||
|
this.taskItem = doSorting<ReplicationTasks>(this.taskItem, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBack(): void {
|
||||||
|
this.router.navigate(["harbor", "replications"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFilter($event: any): void {
|
||||||
|
this.defaultFilter = $event['target'].value;
|
||||||
|
this.doSearch(this.searchTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh icon
|
||||||
|
refreshTasks(): void {
|
||||||
|
this.searchTask = '';
|
||||||
|
this.loading = true;
|
||||||
|
this.replicationService.getReplicationTasks(this.executionId)
|
||||||
|
.subscribe(res => {
|
||||||
|
this.tasks = res;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.loading = false;
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public doSearch(value: string): void {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.searchTask = value.trim();
|
||||||
|
this.loading = true;
|
||||||
|
this.currentPage = 1;
|
||||||
|
if (this.currentPagePvt === 1) {
|
||||||
|
// Force reloading
|
||||||
|
let st: State = this.currentState;
|
||||||
|
if (!st) {
|
||||||
|
st = {
|
||||||
|
page: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
st.page.from = 0;
|
||||||
|
st.page.to = this.pageSize - 1;
|
||||||
|
st.page.size = this.pageSize;
|
||||||
|
|
||||||
|
this.currentPagePvt = 0;
|
||||||
|
|
||||||
|
this.clrLoadTasks(st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openFilter(isOpen: boolean): void {
|
||||||
|
if (isOpen) {
|
||||||
|
this.isOpenFilterTag = true;
|
||||||
|
} else {
|
||||||
|
this.isOpenFilterTag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,56 +23,60 @@
|
|||||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 jobList" [hidden]='hiddenJobList'>
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 jobList" [hidden]='hiddenJobList'>
|
||||||
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<div class="row flex-items-xs-between jobsRow">
|
<div class="row flex-items-xs-between jobsRow">
|
||||||
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
|
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5>
|
||||||
<div class="flex-items-xs-bottom option-right-down">
|
<div class="row flex-items-xs-between flex-items-xs-bottom">
|
||||||
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</button>
|
<div class="execution-select">
|
||||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_JOBS_PLACEHOLDER" | translate}}' (filterEvt)="doSearchJobs($event)"
|
<div class="select filter-tag" [hidden]="!isOpenFilterTag">
|
||||||
[currentValue]="search.repoName"></hbr-filter>
|
<select (change)="doFilterJob($event)">
|
||||||
<span class="refresh-btn" (click)="refreshJobs()">
|
<option value="trigger">{{'REPLICATION.REPLICATION_TRIGGER' |translate}}</option>
|
||||||
<clr-icon shape="refresh"></clr-icon>
|
<option value="status">{{'REPLICATION.STATUS' | translate}}</option>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row flex-items-xs-right row-right" [hidden]="currentJobSearchOption === 0">
|
|
||||||
<div class="select select-status">
|
|
||||||
<select (change)="doFilterJobStatus($event)">
|
|
||||||
<option *ngFor="let j of jobStatus" value="{{j.key}}" [selected]="currentJobStatus.key === j.key">{{j.description | translate}}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-items-xs-middle">
|
<hbr-filter [withDivider]="true" (openFlag)="openFilter($event)"
|
||||||
<hbr-datetime [dateInput]="search.startTime" (search)="doJobSearchByStartTime($event)"></hbr-datetime>
|
filterPlaceholder='{{"REPLICATION.FILTER_EXECUTIONS_PLACEHOLDER" | translate}}'
|
||||||
<hbr-datetime [dateInput]="search.endTime" [oneDayOffset]="true" (search)="doJobSearchByEndTime($event)"></hbr-datetime>
|
(filterEvt)="doSearchExecutions($event)" [currentValue]="currentTerm"></hbr-filter>
|
||||||
|
<span class="refresh-btn">
|
||||||
|
<clr-icon shape="refresh" (click)="refreshJobs()"></clr-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<clr-datagrid [clrDgLoading]="jobsLoading" (clrDgRefresh)="clrLoadJobs($event)">
|
<clr-datagrid [(clrDgSelected)]="selectedRow" [clrDgLoading]="jobsLoading" (clrDgRefresh)="clrLoadJobs($event)">
|
||||||
<clr-dg-action-bar>
|
<clr-dg-action-bar>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!(jobs && jobs.length>0) || isStopOnGoing"
|
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!(jobs && jobs.length>0) || isStopOnGoing || !selectedRow.length"
|
||||||
(click)="stopJobs()">{{'REPLICATION.STOPJOB' | translate}}</button>
|
(click)="openStopExecutionsDialog(selectedRow)">{{'REPLICATION.STOPJOB' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
</clr-dg-action-bar>
|
</clr-dg-action-bar>
|
||||||
<clr-dg-column [clrDgField]="'repository'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgField]="'id'">{{'REPLICATION.ID' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgField]="'trigger'">{{'REPLICATION.REPLICATION_TRIGGER' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'operation'">{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
|
|
||||||
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
|
<clr-dg-column [clrDgSortBy]="creationTimeComparator">{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgSortBy]="updateTimeComparator">{{'REPLICATION.UPDATE_TIME' | translate}}</clr-dg-column>
|
<clr-dg-column>{{'REPLICATION.DURATION' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
|
<clr-dg-column>{{'REPLICATION.SUCCESS_RATE' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column>{{'REPLICATION.TOTAL' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
|
||||||
<clr-dg-placeholder>{{'REPLICATION.JOB_PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
<clr-dg-placeholder>{{'REPLICATION.JOB_PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||||
<clr-dg-row *ngFor="let j of jobs">
|
<clr-dg-row *ngFor="let j of jobs" [clrDgItem]="j">
|
||||||
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
|
|
||||||
<clr-dg-cell>{{j.status}}</clr-dg-cell>
|
|
||||||
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
|
|
||||||
<clr-dg-cell>{{j.creation_time | date: 'short'}}</clr-dg-cell>
|
|
||||||
<clr-dg-cell>{{j.update_time | date: 'short'}}</clr-dg-cell>
|
|
||||||
<clr-dg-cell>
|
<clr-dg-cell>
|
||||||
<span *ngIf="j.status=='pending'; else elseBlock" class="label">{{'REPLICATION.NO_LOGS' | translate}}</span>
|
<a href="javascript:void(0)" (click)="goToLink(j.id)">{{j.id}}</a>
|
||||||
<ng-template #elseBlock>
|
</clr-dg-cell>
|
||||||
<a target="_blank" [href]="viewLog(j.id)">
|
<clr-dg-cell>{{j.trigger}}</clr-dg-cell>
|
||||||
<clr-icon shape="list"></clr-icon>
|
<clr-dg-cell>{{j.start_time | date: 'short'}}</clr-dg-cell>
|
||||||
</a>
|
<clr-dg-cell>{{getDuration(j)}}</clr-dg-cell>
|
||||||
</ng-template>
|
<clr-dg-cell>
|
||||||
|
{{(j.succeed > 0 ? j.succeed / j.total : 0) | percent }}
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{j.total}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
{{j.status}}
|
||||||
|
<clr-tooltip>
|
||||||
|
<clr-icon *ngIf="j.status_text" clrTooltipTrigger shape="info-circle" size="20"></clr-icon>
|
||||||
|
<clr-tooltip-content [clrPosition]="'left'" clrSize="md" *clrIfOpen>
|
||||||
|
<span>{{j.status_text}}</span>
|
||||||
|
</clr-tooltip-content>
|
||||||
|
</clr-tooltip>
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
@ -83,7 +87,8 @@
|
|||||||
</clr-datagrid>
|
</clr-datagrid>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hbr-create-edit-rule *ngIf="isSystemAdmin" [withAdmiral]="withAdmiral" [projectId]="projectId" [projectName]="projectName"
|
<hbr-create-edit-rule *ngIf="isSystemAdmin" [withAdmiral]="withAdmiral"
|
||||||
(goToRegistry)="goRegistry()" (reload)="reloadRules($event)"></hbr-create-edit-rule>
|
(goToRegistry)="goRegistry()" (reload)="reloadRules($event)"></hbr-create-edit-rule>
|
||||||
<confirmation-dialog #replicationConfirmDialog (confirmAction)="confirmReplication($event)"></confirmation-dialog>
|
<confirmation-dialog #replicationConfirmDialog (confirmAction)="confirmReplication($event)"></confirmation-dialog>
|
||||||
|
<confirmation-dialog #StopConfirmDialog (confirmAction)="confirmStop($event)"></confirmation-dialog>
|
||||||
</div>
|
</div>
|
@ -13,6 +13,25 @@
|
|||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.execution-select {
|
||||||
|
padding-right: 18px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 25px;
|
||||||
|
.filter-tag {
|
||||||
|
float: left;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.refresh-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 7px;
|
||||||
|
&:hover {
|
||||||
|
color: #007CBB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rightPos{
|
.rightPos{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 35px;
|
right: 35px;
|
||||||
@ -28,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row-right {
|
.row-right {
|
||||||
padding-right: 50px;
|
margin-left: 564px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.replication-row {
|
.replication-row {
|
||||||
@ -49,3 +68,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clr-datagrid {
|
||||||
|
::ng-deep .datagrid-table {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
@ -22,167 +22,106 @@ import {ProjectDefaultService, ProjectService} from "../service/project.service"
|
|||||||
import {OperationService} from "../operation/operation.service";
|
import {OperationService} from "../operation/operation.service";
|
||||||
import {FilterLabelComponent} from "../create-edit-rule/filter-label.component";
|
import {FilterLabelComponent} from "../create-edit-rule/filter-label.component";
|
||||||
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
describe('Replication Component (inline template)', () => {
|
describe('Replication Component (inline template)', () => {
|
||||||
|
|
||||||
let mockRules: ReplicationRule[] = [
|
let mockRules: ReplicationRule[] = [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"projects": [{
|
|
||||||
"project_id": 33,
|
|
||||||
"owner_id": 1,
|
|
||||||
"name": "aeas",
|
|
||||||
"deleted": 0,
|
|
||||||
"togglable": false,
|
|
||||||
"current_user_role_id": 0,
|
|
||||||
"repo_count": 0,
|
|
||||||
"metadata": {
|
|
||||||
"public": false,
|
|
||||||
"enable_content_trust": "",
|
|
||||||
"prevent_vul": "",
|
|
||||||
"severity": "",
|
|
||||||
"auto_scan": ""},
|
|
||||||
"owner_name": "",
|
|
||||||
"creation_time": null,
|
|
||||||
"update_time": null,
|
|
||||||
"has_project_admin_role": true,
|
|
||||||
"is_member": true,
|
|
||||||
"role_name": ""
|
|
||||||
}],
|
|
||||||
"targets": [{
|
|
||||||
"id": 1,
|
|
||||||
"endpoint": "https://10.117.4.151",
|
|
||||||
"name": "target_01",
|
|
||||||
"username": "admin",
|
|
||||||
"password": "",
|
|
||||||
"insecure": false,
|
|
||||||
"type": 0
|
|
||||||
}],
|
|
||||||
"name": "sync_01",
|
"name": "sync_01",
|
||||||
"description": "",
|
"description": "",
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
"trigger": {"type": "Manual", "trigger_settings": null},
|
||||||
"error_job_count": 2,
|
"error_job_count": 2,
|
||||||
"replicate_deletion": false,
|
"deletion": false,
|
||||||
"replicate_existing_image_now": false,
|
"src_registry": {id: 3},
|
||||||
|
"src_namespaces": ["name1"],
|
||||||
|
"enabled": true,
|
||||||
|
"override": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"projects": [{
|
|
||||||
"project_id": 33,
|
|
||||||
"owner_id": 1,
|
|
||||||
"name": "aeas",
|
|
||||||
"deleted": 0,
|
|
||||||
"togglable": false,
|
|
||||||
"current_user_role_id": 0,
|
|
||||||
"repo_count": 0,
|
|
||||||
"metadata": {
|
|
||||||
"public": false,
|
|
||||||
"enable_content_trust": "",
|
|
||||||
"prevent_vul": "",
|
|
||||||
"severity": "",
|
|
||||||
"auto_scan": ""},
|
|
||||||
"owner_name": "",
|
|
||||||
"creation_time": null,
|
|
||||||
"update_time": null,
|
|
||||||
"has_project_admin_role": true,
|
|
||||||
"is_member": true,
|
|
||||||
"role_name": ""
|
|
||||||
}],
|
|
||||||
"targets": [{
|
|
||||||
"id": 1,
|
|
||||||
"endpoint": "https://10.117.4.151",
|
|
||||||
"name": "target_01",
|
|
||||||
"username": "admin",
|
|
||||||
"password": "",
|
|
||||||
"insecure": false,
|
|
||||||
"type": 0
|
|
||||||
}],
|
|
||||||
"name": "sync_02",
|
"name": "sync_02",
|
||||||
"description": "",
|
"description": "",
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
"trigger": {"type": "Manual", "trigger_settings": null},
|
||||||
"error_job_count": 2,
|
"error_job_count": 2,
|
||||||
"replicate_deletion": false,
|
"deletion": false,
|
||||||
"replicate_existing_image_now": false,
|
"dest_registry": {id: 5},
|
||||||
|
"src_namespaces": ["name1"],
|
||||||
|
"enabled": true,
|
||||||
|
"override": true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
let mockJobs: ReplicationJobItem[] = [
|
let mockJobs: ReplicationJobItem[] = [
|
||||||
{
|
{
|
||||||
"id": 1,
|
id: 1,
|
||||||
"status": "error",
|
status: "stopped",
|
||||||
"repository": "library/nginx",
|
policy_id: 1,
|
||||||
"policy_id": 1,
|
trigger: "Manual",
|
||||||
"operation": "transfer",
|
total: 0,
|
||||||
"update_time": new Date("2017-05-23 12:20:33"),
|
failed: 0,
|
||||||
"tags": null
|
succeed: 0,
|
||||||
|
in_progress: 0,
|
||||||
|
stopped: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"status": "finished",
|
status: "stopped",
|
||||||
"repository": "library/mysql",
|
policy_id: 1,
|
||||||
"policy_id": 1,
|
trigger: "Manual",
|
||||||
"operation": "transfer",
|
total: 1,
|
||||||
"update_time": new Date("2017-05-27 12:20:33"),
|
failed: 0,
|
||||||
"tags": null
|
succeed: 1,
|
||||||
|
in_progress: 0,
|
||||||
|
stopped: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"status": "stopped",
|
status: "stopped",
|
||||||
"repository": "library/busybox",
|
policy_id: 2,
|
||||||
"policy_id": 2,
|
trigger: "Manual",
|
||||||
"operation": "transfer",
|
total: 1,
|
||||||
"update_time": new Date("2017-04-23 12:20:33"),
|
failed: 1,
|
||||||
"tags": null
|
succeed: 0,
|
||||||
|
in_progress: 0,
|
||||||
|
stopped: 0
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
let mockEndpoints: Endpoint[] = [
|
let mockEndpoints: Endpoint[] = [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"endpoint": "https://10.117.4.151",
|
"credential": {
|
||||||
"name": "target_01",
|
"access_key": "admin",
|
||||||
"username": "admin",
|
"access_secret": "",
|
||||||
"password": "",
|
"type": "basic"
|
||||||
|
},
|
||||||
|
"description": "test",
|
||||||
"insecure": false,
|
"insecure": false,
|
||||||
"type": 0
|
"name": "target_01",
|
||||||
|
"type": "Harbor",
|
||||||
|
"url": "https://10.117.4.151"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"endpoint": "https://10.117.5.142",
|
"credential": {
|
||||||
"name": "target_02",
|
"access_key": "admin",
|
||||||
"username": "AAA",
|
"access_secret": "",
|
||||||
"password": "",
|
"type": "basic"
|
||||||
|
},
|
||||||
|
"description": "test",
|
||||||
"insecure": false,
|
"insecure": false,
|
||||||
"type": 0
|
"name": "target_02",
|
||||||
|
"type": "Harbor",
|
||||||
|
"url": "https://10.117.5.142"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// let mockProjects: Project[] = [
|
|
||||||
// { "project_id": 1,
|
|
||||||
// "owner_id": 0,
|
|
||||||
// "name": 'project_01',
|
|
||||||
// "creation_time": '',
|
|
||||||
// "deleted": 0,
|
|
||||||
// "owner_name": '',
|
|
||||||
// "togglable": false,
|
|
||||||
// "update_time": '',
|
|
||||||
// "current_user_role_id": 0,
|
|
||||||
// "repo_count": 0,
|
|
||||||
// "has_project_admin_role": false,
|
|
||||||
// "is_member": false,
|
|
||||||
// "role_name": '',
|
|
||||||
// "metadata": {
|
|
||||||
// "public": '',
|
|
||||||
// "enable_content_trust": '',
|
|
||||||
// "prevent_vul": '',
|
|
||||||
// "severity": '',
|
|
||||||
// "auto_scan": '',
|
|
||||||
// }
|
|
||||||
// }];
|
|
||||||
|
|
||||||
let mockJob: ReplicationJob = {
|
let mockJob: ReplicationJob = {
|
||||||
metadata: {xTotalCount: 3},
|
metadata: {xTotalCount: 3},
|
||||||
data: mockJobs
|
data: mockJobs
|
||||||
@ -198,7 +137,7 @@ describe('Replication Component (inline template)', () => {
|
|||||||
|
|
||||||
let spyRules: jasmine.Spy;
|
let spyRules: jasmine.Spy;
|
||||||
let spyJobs: jasmine.Spy;
|
let spyJobs: jasmine.Spy;
|
||||||
let spyEndpoint: jasmine.Spy;
|
let spyEndpoints: jasmine.Spy;
|
||||||
|
|
||||||
let deGrids: DebugElement[];
|
let deGrids: DebugElement[];
|
||||||
let deRules: DebugElement;
|
let deRules: DebugElement;
|
||||||
@ -208,15 +147,15 @@ describe('Replication Component (inline template)', () => {
|
|||||||
let elJob: HTMLElement;
|
let elJob: HTMLElement;
|
||||||
|
|
||||||
let config: IServiceConfig = {
|
let config: IServiceConfig = {
|
||||||
replicationRuleEndpoint: '/api/policies/replication/testing',
|
replicationRuleEndpoint: '/api/policies/replication/testing'
|
||||||
replicationJobEndpoint: '/api/jobs/replication/testing'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
NoopAnimationsModule
|
NoopAnimationsModule,
|
||||||
|
RouterTestingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ReplicationComponent,
|
ReplicationComponent,
|
||||||
@ -254,10 +193,10 @@ describe('Replication Component (inline template)', () => {
|
|||||||
endpointService = fixtureCreate.debugElement.injector.get(EndpointService);
|
endpointService = fixtureCreate.debugElement.injector.get(EndpointService);
|
||||||
|
|
||||||
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(of(mockRules));
|
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(of(mockRules));
|
||||||
spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(of(mockJob));
|
spyJobs = spyOn(replicationService, 'getExecutions').and.returnValues(of(mockJob));
|
||||||
|
|
||||||
spyEndpoint = spyOn(endpointService, 'getEndpoints').and.returnValues(of(mockEndpoints));
|
|
||||||
|
|
||||||
|
spyEndpoints = spyOn(endpointService, 'getEndpoints').and.returnValues(of(mockEndpoints));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@ -325,7 +264,6 @@ describe('Replication Component (inline template)', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
comp.doFilterJobStatus('finished');
|
|
||||||
let el: HTMLElement = deJobs.nativeElement;
|
let el: HTMLElement = deJobs.nativeElement;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(el).toBeTruthy();
|
expect(el).toBeTruthy();
|
||||||
@ -337,8 +275,6 @@ describe('Replication Component (inline template)', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
comp.doJobSearchByStartTime('2017-05-01');
|
|
||||||
comp.doJobSearchByEndTime('2015-05-25');
|
|
||||||
let el: HTMLElement = deJobs.nativeElement;
|
let el: HTMLElement = deJobs.nativeElement;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(el).toBeTruthy();
|
expect(el).toBeTruthy();
|
||||||
|
@ -21,9 +21,8 @@ import {
|
|||||||
EventEmitter
|
EventEmitter
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { Comparator, State } from "../service/interface";
|
import { Comparator, State } from "../service/interface";
|
||||||
import { Subscription, forkJoin, timer, Observable} from "rxjs";
|
import { finalize, catchError, map } from "rxjs/operators";
|
||||||
|
import { Subscription, forkJoin, timer, Observable, throwError } from "rxjs";
|
||||||
|
|
||||||
import { TranslateService } from "@ngx-translate/core";
|
import { TranslateService } from "@ngx-translate/core";
|
||||||
|
|
||||||
import { ListReplicationRuleComponent } from "../list-replication-rule/list-replication-rule.component";
|
import { ListReplicationRuleComponent } from "../list-replication-rule/list-replication-rule.component";
|
||||||
@ -54,41 +53,28 @@ import {
|
|||||||
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
|
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
|
||||||
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
|
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
|
||||||
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
|
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
|
||||||
import {operateChanges, OperationState, OperateInfo} from "../operation/operate";
|
import {
|
||||||
import {OperationService} from "../operation/operation.service";
|
operateChanges,
|
||||||
import { catchError, map } from "rxjs/operators";
|
OperationState,
|
||||||
import { throwError as observableThrowError } from "rxjs";
|
OperateInfo
|
||||||
|
} from "../operation/operate";
|
||||||
|
import { OperationService } from "../operation/operation.service";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
const ONE_HOUR_SECONDS: number = 3600;
|
||||||
|
const ONE_MINUTE_SECONDS: number = 60;
|
||||||
|
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
|
||||||
|
|
||||||
const ruleStatus: { [key: string]: any } = [
|
const ruleStatus: { [key: string]: any } = [
|
||||||
{ key: "all", description: "REPLICATION.ALL_STATUS" },
|
{ key: "all", description: "REPLICATION.ALL_STATUS" },
|
||||||
{ key: "1", description: "REPLICATION.ENABLED" },
|
{ key: "1", description: "REPLICATION.ENABLED" },
|
||||||
{ key: "0", description: "REPLICATION.DISABLED" }
|
{ key: "0", description: "REPLICATION.DISABLED" }
|
||||||
];
|
];
|
||||||
|
|
||||||
const jobStatus: { [key: string]: any } = [
|
|
||||||
{ key: "all", description: "REPLICATION.ALL" },
|
|
||||||
{ key: "pending", description: "REPLICATION.PENDING" },
|
|
||||||
{ key: "running", description: "REPLICATION.RUNNING" },
|
|
||||||
{ key: "error", description: "REPLICATION.ERROR" },
|
|
||||||
{ key: "retrying", description: "REPLICATION.RETRYING" },
|
|
||||||
{ key: "stopped", description: "REPLICATION.STOPPED" },
|
|
||||||
{ key: "finished", description: "REPLICATION.FINISHED" },
|
|
||||||
{ key: "canceled", description: "REPLICATION.CANCELED" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const optionalSearch: {} = {
|
|
||||||
0: "REPLICATION.ADVANCED",
|
|
||||||
1: "REPLICATION.SIMPLE"
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SearchOption {
|
export class SearchOption {
|
||||||
ruleId: number | string;
|
ruleId: number | string;
|
||||||
ruleName: string = "";
|
ruleName: string = "";
|
||||||
repoName: string = "";
|
trigger: string = "";
|
||||||
status: string = "";
|
status: string = "";
|
||||||
startTime: string = "";
|
|
||||||
startTimestamp: string = "";
|
|
||||||
endTime: string = "";
|
|
||||||
endTimestamp: string = "";
|
|
||||||
page: number = 1;
|
page: number = 1;
|
||||||
pageSize: number = DEFAULT_PAGE_SIZE;
|
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||||
}
|
}
|
||||||
@ -115,15 +101,16 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
@Output() goToRegistry = new EventEmitter<any>();
|
@Output() goToRegistry = new EventEmitter<any>();
|
||||||
|
|
||||||
search: SearchOption = new SearchOption();
|
search: SearchOption = new SearchOption();
|
||||||
|
isOpenFilterTag: boolean;
|
||||||
ruleStatus = ruleStatus;
|
ruleStatus = ruleStatus;
|
||||||
currentRuleStatus: { key: string; description: string };
|
currentRuleStatus: { key: string; description: string };
|
||||||
|
|
||||||
jobStatus = jobStatus;
|
currentTerm: string;
|
||||||
currentJobStatus: { key: string; description: string };
|
defaultFilter = "trigger";
|
||||||
|
|
||||||
changedRules: ReplicationRule[];
|
changedRules: ReplicationRule[];
|
||||||
|
|
||||||
|
selectedRow: ReplicationJobItem[] = [];
|
||||||
rules: ReplicationRule[];
|
rules: ReplicationRule[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
isStopOnGoing: boolean;
|
isStopOnGoing: boolean;
|
||||||
@ -131,25 +118,24 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
jobs: ReplicationJobItem[];
|
jobs: ReplicationJobItem[];
|
||||||
|
|
||||||
toggleJobSearchOption = optionalSearch;
|
|
||||||
currentJobSearchOption: number;
|
|
||||||
|
|
||||||
@ViewChild(ListReplicationRuleComponent)
|
@ViewChild(ListReplicationRuleComponent)
|
||||||
listReplicationRule: ListReplicationRuleComponent;
|
listReplicationRule: ListReplicationRuleComponent;
|
||||||
|
|
||||||
@ViewChild(CreateEditRuleComponent)
|
@ViewChild(CreateEditRuleComponent)
|
||||||
createEditPolicyComponent: CreateEditRuleComponent;
|
createEditPolicyComponent: CreateEditRuleComponent;
|
||||||
|
|
||||||
|
|
||||||
@ViewChild("replicationConfirmDialog")
|
@ViewChild("replicationConfirmDialog")
|
||||||
replicationConfirmDialog: ConfirmationDialogComponent;
|
replicationConfirmDialog: ConfirmationDialogComponent;
|
||||||
|
|
||||||
|
@ViewChild("StopConfirmDialog")
|
||||||
|
StopConfirmDialog: ConfirmationDialogComponent;
|
||||||
|
|
||||||
creationTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
|
creationTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
|
||||||
ReplicationJob
|
ReplicationJob
|
||||||
>("creation_time", "date");
|
>("start_time", "date");
|
||||||
updateTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
|
updateTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
|
||||||
ReplicationJob
|
ReplicationJob
|
||||||
>("update_time", "date");
|
>("end_time", "date");
|
||||||
|
|
||||||
// Server driven pagination
|
// Server driven pagination
|
||||||
currentPage: number = 1;
|
currentPage: number = 1;
|
||||||
@ -160,10 +146,12 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
timerDelay: Subscription;
|
timerDelay: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private router: Router,
|
||||||
private errorHandler: ErrorHandler,
|
private errorHandler: ErrorHandler,
|
||||||
private replicationService: ReplicationService,
|
private replicationService: ReplicationService,
|
||||||
private operationService: OperationService,
|
private operationService: OperationService,
|
||||||
private translateService: TranslateService) {}
|
private translateService: TranslateService
|
||||||
|
) {}
|
||||||
|
|
||||||
public get showPaginationIndex(): boolean {
|
public get showPaginationIndex(): boolean {
|
||||||
return this.totalCount > 0;
|
return this.totalCount > 0;
|
||||||
@ -171,8 +159,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.currentRuleStatus = this.ruleStatus[0];
|
this.currentRuleStatus = this.ruleStatus[0];
|
||||||
this.currentJobStatus = this.jobStatus[0];
|
|
||||||
this.currentJobSearchOption = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -197,6 +183,11 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
this.goToRegistry.emit();
|
this.goToRegistry.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToLink(exeId: number): void {
|
||||||
|
let linkUrl = ["harbor", "replications", exeId, "tasks"];
|
||||||
|
this.router.navigate(linkUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// Server driven data loading
|
// Server driven data loading
|
||||||
clrLoadJobs(state: State): void {
|
clrLoadJobs(state: State): void {
|
||||||
if (!state || !state.page || !this.search.ruleId) {
|
if (!state || !state.page || !this.search.ruleId) {
|
||||||
@ -213,42 +204,23 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
// Pagination
|
// Pagination
|
||||||
params.set("page", "" + pageNumber);
|
params.set("page", "" + pageNumber);
|
||||||
params.set("page_size", "" + this.pageSize);
|
params.set("page_size", "" + this.pageSize);
|
||||||
// Search by status
|
|
||||||
if (this.search.status.trim()) {
|
if (this.currentTerm && this.currentTerm !== "") {
|
||||||
params.set("status", this.search.status);
|
params.set(this.defaultFilter, this.currentTerm);
|
||||||
}
|
|
||||||
// Search by repository
|
|
||||||
if (this.search.repoName.trim()) {
|
|
||||||
params.set("repository", this.search.repoName);
|
|
||||||
}
|
|
||||||
// Search by timestamps
|
|
||||||
if (this.search.startTimestamp.trim()) {
|
|
||||||
params.set("start_time", this.search.startTimestamp);
|
|
||||||
}
|
|
||||||
if (this.search.endTimestamp.trim()) {
|
|
||||||
params.set("end_time", this.search.endTimestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.jobsLoading = true;
|
this.jobsLoading = true;
|
||||||
|
|
||||||
// Do filtering and sorting
|
this.replicationService.getExecutions(this.search.ruleId, params).subscribe(
|
||||||
this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state);
|
response => {
|
||||||
this.jobs = doSorting<ReplicationJobItem>(this.jobs, state);
|
|
||||||
|
|
||||||
this.jobsLoading = false;
|
|
||||||
this.replicationService.getJobs(this.search.ruleId, params)
|
|
||||||
.subscribe(response => {
|
|
||||||
this.totalCount = response.metadata.xTotalCount;
|
this.totalCount = response.metadata.xTotalCount;
|
||||||
this.jobs = response.data;
|
this.jobs = response.data;
|
||||||
|
|
||||||
if (!this.timerDelay) {
|
if (!this.timerDelay) {
|
||||||
this.timerDelay = timer(10000, 10000).subscribe(() => {
|
this.timerDelay = timer(10000, 10000).subscribe(() => {
|
||||||
let count: number = 0;
|
let count: number = 0;
|
||||||
this.jobs.forEach(job => {
|
this.jobs.forEach(job => {
|
||||||
if (
|
if (
|
||||||
job.status === "pending" ||
|
job.status === "InProgress"
|
||||||
job.status === "running" ||
|
|
||||||
job.status === "retrying"
|
|
||||||
) {
|
) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
@ -261,18 +233,30 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do filtering and sorting
|
// Do filtering and sorting
|
||||||
this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state);
|
this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state);
|
||||||
this.jobs = doSorting<ReplicationJobItem>(this.jobs, state);
|
this.jobs = doSorting<ReplicationJobItem>(this.jobs, state);
|
||||||
|
|
||||||
this.jobsLoading = false;
|
this.jobsLoading = false;
|
||||||
}, error => {
|
},
|
||||||
|
error => {
|
||||||
this.jobsLoading = false;
|
this.jobsLoading = false;
|
||||||
this.errorHandler.error(error);
|
this.errorHandler.error(error);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public doSearchExecutions(terms: string): void {
|
||||||
|
if (!terms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentTerm = terms.trim();
|
||||||
|
// Trigger data loading and start from first page
|
||||||
|
this.jobsLoading = true;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.jobsLoading = true;
|
||||||
|
// Force reloading
|
||||||
|
this.loadFirstPage();
|
||||||
|
}
|
||||||
loadFirstPage(): void {
|
loadFirstPage(): void {
|
||||||
let st: State = this.currentState;
|
let st: State = this.currentState;
|
||||||
if (!st) {
|
if (!st) {
|
||||||
@ -291,10 +275,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
if (rule && rule.id) {
|
if (rule && rule.id) {
|
||||||
this.hiddenJobList = false;
|
this.hiddenJobList = false;
|
||||||
this.search.ruleId = rule.id || "";
|
this.search.ruleId = rule.id || "";
|
||||||
this.search.repoName = "";
|
|
||||||
this.search.status = "";
|
|
||||||
this.currentJobSearchOption = 0;
|
|
||||||
this.currentJobStatus = { key: "all", description: "REPLICATION.ALL" };
|
|
||||||
this.loadFirstPage();
|
this.loadFirstPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,7 +302,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
let rule: ReplicationRule = message.data;
|
let rule: ReplicationRule = message.data;
|
||||||
|
|
||||||
if (rule) {
|
if (rule) {
|
||||||
forkJoin(this.replicationOperate(rule)).subscribe((item) => {
|
forkJoin(this.replicationOperate(rule)).subscribe(item => {
|
||||||
this.selectOneRule(rule);
|
this.selectOneRule(rule);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -332,30 +312,39 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
replicationOperate(rule: ReplicationRule): Observable<any> {
|
replicationOperate(rule: ReplicationRule): Observable<any> {
|
||||||
// init operation info
|
// init operation info
|
||||||
let operMessage = new OperateInfo();
|
let operMessage = new OperateInfo();
|
||||||
operMessage.name = 'OPERATION.REPLICATION';
|
operMessage.name = "OPERATION.REPLICATION";
|
||||||
operMessage.data.id = rule.id;
|
operMessage.data.id = rule.id;
|
||||||
operMessage.state = OperationState.progressing;
|
operMessage.state = OperationState.progressing;
|
||||||
operMessage.data.name = rule.name;
|
operMessage.data.name = rule.name;
|
||||||
this.operationService.publishInfo(operMessage);
|
this.operationService.publishInfo(operMessage);
|
||||||
|
|
||||||
return this.replicationService.replicateRule(+rule.id)
|
return this.replicationService.replicateRule(+rule.id).pipe(
|
||||||
.pipe(map(response => {
|
map(response => {
|
||||||
this.translateService.get('BATCH.REPLICATE_SUCCESS')
|
this.translateService
|
||||||
.subscribe(res => operateChanges(operMessage, OperationState.success));
|
.get("BATCH.REPLICATE_SUCCESS")
|
||||||
})
|
.subscribe(res =>
|
||||||
, catchError(error => {
|
operateChanges(operMessage, OperationState.success)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
if (error && error.status === 412) {
|
if (error && error.status === 412) {
|
||||||
return forkJoin(this.translateService.get('BATCH.REPLICATE_FAILURE'),
|
return forkJoin(
|
||||||
this.translateService.get('REPLICATION.REPLICATE_SUMMARY_FAILURE'))
|
this.translateService.get("BATCH.REPLICATE_FAILURE"),
|
||||||
.pipe(map(function (res) {
|
this.translateService.get("REPLICATION.REPLICATE_SUMMARY_FAILURE")
|
||||||
|
).pipe(
|
||||||
|
map(function(res) {
|
||||||
operateChanges(operMessage, OperationState.failure, res[1]);
|
operateChanges(operMessage, OperationState.failure, res[1]);
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return this.translateService.get('BATCH.REPLICATE_FAILURE').pipe(map(res => {
|
return this.translateService.get("BATCH.REPLICATE_FAILURE").pipe(
|
||||||
|
map(res => {
|
||||||
operateChanges(operMessage, OperationState.failure, res);
|
operateChanges(operMessage, OperationState.failure, res);
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
customRedirect(rule: ReplicationRule) {
|
customRedirect(rule: ReplicationRule) {
|
||||||
@ -367,21 +356,17 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
this.listReplicationRule.retrieveRules(ruleName);
|
this.listReplicationRule.retrieveRules(ruleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
doFilterJobStatus($event: any) {
|
doFilterJob($event: any): void {
|
||||||
if ($event && $event.target && $event.target["value"]) {
|
this.defaultFilter = $event["target"].value;
|
||||||
let status = $event.target["value"];
|
this.doSearchJobs(this.currentTerm);
|
||||||
|
|
||||||
this.currentJobStatus = this.jobStatus.find((r: any) => r.key === status);
|
|
||||||
if (this.currentJobStatus.key === "all") {
|
|
||||||
status = "";
|
|
||||||
}
|
|
||||||
this.search.status = status;
|
|
||||||
this.doSearchJobs(this.search.repoName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doSearchJobs(repoName: string) {
|
doSearchJobs(terms: string) {
|
||||||
this.search.repoName = repoName;
|
if (!terms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentTerm = terms.trim();
|
||||||
|
this.currentPage = 1;
|
||||||
this.loadFirstPage();
|
this.loadFirstPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,17 +376,68 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
this.hiddenJobList = true;
|
this.hiddenJobList = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
stopJobs() {
|
openStopExecutionsDialog(targets: ReplicationJobItem[]) {
|
||||||
if (this.jobs && this.jobs.length) {
|
let ExecutionId = targets.map(robot => robot.id).join(",");
|
||||||
|
let StopExecutionsMessage = new ConfirmationMessage(
|
||||||
|
"REPLICATION.STOP_TITLE",
|
||||||
|
"REPLICATION.STOP_SUMMARY",
|
||||||
|
ExecutionId,
|
||||||
|
targets,
|
||||||
|
ConfirmationTargets.STOP_EXECUTIONS,
|
||||||
|
ConfirmationButtons.STOP_CANCEL
|
||||||
|
);
|
||||||
|
this.StopConfirmDialog.open(StopExecutionsMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmStop(message: ConfirmationAcknowledgement) {
|
||||||
|
if (
|
||||||
|
message &&
|
||||||
|
message.state === ConfirmationState.CONFIRMED &&
|
||||||
|
message.source === ConfirmationTargets.STOP_EXECUTIONS
|
||||||
|
) {
|
||||||
|
this.StopExecutions(message.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StopExecutions(targets: ReplicationJobItem[]): void {
|
||||||
|
if (targets && targets.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.isStopOnGoing = true;
|
this.isStopOnGoing = true;
|
||||||
this.replicationService.stopJobs(this.jobs[0].policy_id)
|
if (this.jobs && this.jobs.length) {
|
||||||
.subscribe(res => {
|
let ExecutionsStop$ = targets.map(target => this.StopOperate(target));
|
||||||
|
forkJoin(ExecutionsStop$)
|
||||||
|
.pipe(
|
||||||
|
catchError(err => throwError(err)),
|
||||||
|
finalize(() => {
|
||||||
this.refreshJobs();
|
this.refreshJobs();
|
||||||
this.isStopOnGoing = false;
|
this.isStopOnGoing = false;
|
||||||
}, error => this.errorHandler.error(error));
|
this.selectedRow = [];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StopOperate(targets: ReplicationJobItem): any {
|
||||||
|
let operMessage = new OperateInfo();
|
||||||
|
operMessage.name = "OPERATION.STOP_EXECUTIONS";
|
||||||
|
operMessage.data.id = targets.id;
|
||||||
|
operMessage.state = OperationState.progressing;
|
||||||
|
operMessage.data.name = targets.id;
|
||||||
|
this.operationService.publishInfo(operMessage);
|
||||||
|
|
||||||
|
return this.replicationService
|
||||||
|
.stopJobs(targets.id)
|
||||||
|
.pipe(
|
||||||
|
map(
|
||||||
|
() => operateChanges(operMessage, OperationState.success),
|
||||||
|
err => operateChanges(operMessage, OperationState.failure, err)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
reloadRules(isReady: boolean) {
|
reloadRules(isReady: boolean) {
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
this.search.ruleName = "";
|
this.search.ruleName = "";
|
||||||
@ -414,14 +450,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshJobs() {
|
refreshJobs() {
|
||||||
this.currentJobStatus = this.jobStatus[0];
|
this.currentTerm = "";
|
||||||
this.search.startTime = " ";
|
|
||||||
this.search.endTime = " ";
|
|
||||||
this.search.repoName = "";
|
|
||||||
this.search.startTimestamp = "";
|
|
||||||
this.search.endTimestamp = "";
|
|
||||||
this.search.status = "";
|
|
||||||
|
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
|
|
||||||
let st: State = {
|
let st: State = {
|
||||||
@ -434,23 +463,36 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||||||
this.clrLoadJobs(st);
|
this.clrLoadJobs(st);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSearchJobOptionalName(option: number) {
|
openFilter(isOpen: boolean): void {
|
||||||
option === 1
|
if (isOpen) {
|
||||||
? (this.currentJobSearchOption = 0)
|
this.isOpenFilterTag = true;
|
||||||
: (this.currentJobSearchOption = 1);
|
} else {
|
||||||
|
this.isOpenFilterTag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getDuration(j: ReplicationJobItem) {
|
||||||
|
if (!j) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (j.status === "Failed") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
let start_time = new Date(j.start_time).getTime();
|
||||||
|
let end_time = new Date(j.end_time).getTime();
|
||||||
|
let timesDiff = end_time - start_time;
|
||||||
|
let timesDiffSeconds = timesDiff / 1000;
|
||||||
|
let minutes = Math.floor(((timesDiffSeconds % ONE_DAY_SECONDS) % ONE_HOUR_SECONDS) / ONE_MINUTE_SECONDS);
|
||||||
|
let seconds = Math.floor(timesDiffSeconds % ONE_MINUTE_SECONDS);
|
||||||
|
if (minutes > 0) {
|
||||||
|
return minutes + "m" + seconds + "s";
|
||||||
}
|
}
|
||||||
|
|
||||||
doJobSearchByStartTime(fromTimestamp: string) {
|
if (seconds > 0) {
|
||||||
this.search.startTimestamp = fromTimestamp;
|
return seconds + "s";
|
||||||
this.loadFirstPage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doJobSearchByEndTime(toTimestamp: string) {
|
if (seconds <= 0 && timesDiff > 0) {
|
||||||
this.search.endTimestamp = toTimestamp;
|
return timesDiff + 'ms';
|
||||||
this.loadFirstPage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLog(jobId: number | string): string {
|
|
||||||
return this.replicationService.getJobBaseUrl() + "/" + jobId + "/log";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { InjectionToken } from '@angular/core';
|
|||||||
|
|
||||||
export let SERVICE_CONFIG = new InjectionToken("service.config");
|
export let SERVICE_CONFIG = new InjectionToken("service.config");
|
||||||
export interface IServiceConfig {
|
export interface IServiceConfig {
|
||||||
|
baseEndpoint?: string;
|
||||||
/**
|
/**
|
||||||
* The base endpoint of service used to retrieve the system configuration information.
|
* The base endpoint of service used to retrieve the system configuration information.
|
||||||
* The configurations may include but not limit:
|
* The configurations may include but not limit:
|
||||||
@ -66,16 +67,6 @@ export interface IServiceConfig {
|
|||||||
*/
|
*/
|
||||||
replicationRuleEndpoint?: string;
|
replicationRuleEndpoint?: string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base endpoint of the service used to handle the replication jobs.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* * {string}
|
|
||||||
* @memberOf IServiceConfig
|
|
||||||
*/
|
|
||||||
replicationJobEndpoint?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base endpoint of the service used to handle vulnerability scanning.
|
* The base endpoint of the service used to handle vulnerability scanning.
|
||||||
*
|
*
|
||||||
|
@ -12,6 +12,7 @@ import { RequestQueryParams } from "./RequestQueryParams";
|
|||||||
import { Endpoint, ReplicationRule } from "./interface";
|
import { Endpoint, ReplicationRule } from "./interface";
|
||||||
import { catchError, map } from "rxjs/operators";
|
import { catchError, map } from "rxjs/operators";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the service methods to handle the endpoint related things.
|
* Define the service methods to handle the endpoint related things.
|
||||||
*
|
*
|
||||||
@ -58,6 +59,17 @@ export abstract class EndpointService {
|
|||||||
*
|
*
|
||||||
* @memberOf EndpointService
|
* @memberOf EndpointService
|
||||||
*/
|
*/
|
||||||
|
abstract getAdapters(): Observable<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new endpoint.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* ** deprecated param {Adapter} adapter
|
||||||
|
* returns {(Observable<any> | any)}
|
||||||
|
*
|
||||||
|
* @memberOf EndpointService
|
||||||
|
*/
|
||||||
abstract createEndpoint(
|
abstract createEndpoint(
|
||||||
endpoint: Endpoint
|
endpoint: Endpoint
|
||||||
): Observable<any>;
|
): Observable<any>;
|
||||||
@ -133,7 +145,7 @@ export class EndpointDefaultService extends EndpointService {
|
|||||||
super();
|
super();
|
||||||
this._endpointUrl = config.targetBaseEndpoint
|
this._endpointUrl = config.targetBaseEndpoint
|
||||||
? config.targetBaseEndpoint
|
? config.targetBaseEndpoint
|
||||||
: "/api/targets";
|
: "/api/registries";
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEndpoints(
|
public getEndpoints(
|
||||||
@ -166,6 +178,13 @@ export class EndpointDefaultService extends EndpointService {
|
|||||||
, catchError(error => observableThrowError(error)));
|
, catchError(error => observableThrowError(error)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAdapters(): Observable<any> {
|
||||||
|
return this.http
|
||||||
|
.get(`/api/replication/adapters`)
|
||||||
|
.pipe(map(response => response.json())
|
||||||
|
, catchError(error => observableThrowError(error)));
|
||||||
|
}
|
||||||
|
|
||||||
public createEndpoint(
|
public createEndpoint(
|
||||||
endpoint: Endpoint
|
endpoint: Endpoint
|
||||||
): Observable<any> {
|
): Observable<any> {
|
||||||
|
@ -75,13 +75,22 @@ export interface Tag extends Base {
|
|||||||
* extends {Base}
|
* extends {Base}
|
||||||
*/
|
*/
|
||||||
export interface Endpoint extends Base {
|
export interface Endpoint extends Base {
|
||||||
endpoint: string;
|
credential: {
|
||||||
name: string;
|
access_key?: string,
|
||||||
username?: string;
|
access_secret?: string,
|
||||||
password?: string;
|
type: string;
|
||||||
|
};
|
||||||
|
description: string;
|
||||||
insecure: boolean;
|
insecure: boolean;
|
||||||
type: number;
|
name: string;
|
||||||
[key: string]: any;
|
type: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
type: string;
|
||||||
|
style: string;
|
||||||
|
values ?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,33 +106,35 @@ export interface ReplicationRule extends Base {
|
|||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
projects: Project[];
|
|
||||||
targets: Endpoint[];
|
|
||||||
trigger: Trigger;
|
trigger: Trigger;
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
replicate_existing_image_now?: boolean;
|
deletion?: boolean;
|
||||||
replicate_deletion?: boolean;
|
src_registry?: any;
|
||||||
|
dest_registry?: any;
|
||||||
|
src_namespaces: string [];
|
||||||
|
dest_namespace?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
override: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Filter {
|
export class Filter {
|
||||||
kind: string;
|
type: string;
|
||||||
pattern: string;
|
value?: string;
|
||||||
constructor(kind: string, pattern: string) {
|
constructor(type: string) {
|
||||||
this.kind = kind;
|
this.type = type;
|
||||||
this.pattern = pattern;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Trigger {
|
export class Trigger {
|
||||||
kind: string;
|
type: string;
|
||||||
schedule_param:
|
trigger_settings:
|
||||||
| any
|
| any
|
||||||
| {
|
| {
|
||||||
[key: string]: any | any[];
|
[key: string]: any | any[];
|
||||||
};
|
};
|
||||||
constructor(kind: string, param: any | { [key: string]: any | any[] }) {
|
constructor(type: string, param: any | { [key: string]: any | any[] }) {
|
||||||
this.kind = kind;
|
this.type = type;
|
||||||
this.schedule_param = param;
|
this.trigger_settings = param;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,13 +157,34 @@ export interface ReplicationJob {
|
|||||||
*/
|
*/
|
||||||
export interface ReplicationJobItem extends Base {
|
export interface ReplicationJobItem extends Base {
|
||||||
[key: string]: any | any[];
|
[key: string]: any | any[];
|
||||||
|
id: number;
|
||||||
status: string;
|
status: string;
|
||||||
repository: string;
|
|
||||||
policy_id: number;
|
policy_id: number;
|
||||||
operation: string;
|
trigger: string;
|
||||||
tags: string;
|
total: number;
|
||||||
|
failed: number;
|
||||||
|
succeed: number;
|
||||||
|
in_progress: number;
|
||||||
|
stopped: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for replication tasks item.
|
||||||
|
*
|
||||||
|
**
|
||||||
|
* interface ReplicationTasks
|
||||||
|
*/
|
||||||
|
export interface ReplicationTasks extends Base {
|
||||||
|
[key: string]: any | any[];
|
||||||
|
operation: string;
|
||||||
|
id: number;
|
||||||
|
execution_id: number;
|
||||||
|
resource_type: string;
|
||||||
|
src_resource: string;
|
||||||
|
dst_resource: string;
|
||||||
|
job_id: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Interface for storing metadata of response.
|
* Interface for storing metadata of response.
|
||||||
*
|
*
|
||||||
|
@ -6,7 +6,7 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
|||||||
|
|
||||||
describe('JobLogService', () => {
|
describe('JobLogService', () => {
|
||||||
const mockConfig: IServiceConfig = {
|
const mockConfig: IServiceConfig = {
|
||||||
replicationJobEndpoint: "/api/jobs/replication/testing",
|
replicationBaseEndpoint: "/api/replication/testing",
|
||||||
scanJobEndpoint: "/api/jobs/scan/testing"
|
scanJobEndpoint: "/api/jobs/scan/testing"
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ describe('JobLogService', () => {
|
|||||||
|
|
||||||
it('should be initialized', inject([JobLogDefaultService], (service: JobLogService) => {
|
it('should be initialized', inject([JobLogDefaultService], (service: JobLogService) => {
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
expect(config.replicationJobEndpoint).toEqual("/api/jobs/replication/testing");
|
expect(config.replicationBaseEndpoint).toEqual("/api/replication/testing");
|
||||||
expect(config.scanJobEndpoint).toEqual("/api/jobs/scan/testing");
|
expect(config.scanJobEndpoint).toEqual("/api/jobs/scan/testing");
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -47,9 +47,9 @@ export class JobLogDefaultService extends JobLogService {
|
|||||||
@Inject(SERVICE_CONFIG) config: IServiceConfig
|
@Inject(SERVICE_CONFIG) config: IServiceConfig
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this._replicationJobBaseUrl = config.replicationJobEndpoint
|
this._replicationJobBaseUrl = config.replicationBaseEndpoint
|
||||||
? config.replicationJobEndpoint
|
? config.replicationBaseEndpoint
|
||||||
: "/api/jobs/replication";
|
: "/api/replication";
|
||||||
this._scanningJobBaseUrl = config.scanJobEndpoint
|
this._scanningJobBaseUrl = config.scanJobEndpoint
|
||||||
? config.scanJobEndpoint
|
? config.scanJobEndpoint
|
||||||
: "/api/jobs/scan";
|
: "/api/jobs/scan";
|
||||||
|
@ -7,7 +7,7 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
|||||||
describe('ReplicationService', () => {
|
describe('ReplicationService', () => {
|
||||||
const mockConfig: IServiceConfig = {
|
const mockConfig: IServiceConfig = {
|
||||||
replicationRuleEndpoint: "/api/policies/replication/testing",
|
replicationRuleEndpoint: "/api/policies/replication/testing",
|
||||||
replicationJobEndpoint: "/api/jobs/replication/testing"
|
replicationBaseEndpoint: "/api/replication/testing"
|
||||||
};
|
};
|
||||||
|
|
||||||
let config: IServiceConfig;
|
let config: IServiceConfig;
|
||||||
@ -38,6 +38,6 @@ describe('ReplicationService', () => {
|
|||||||
it('should inject the right config', () => {
|
it('should inject the right config', () => {
|
||||||
expect(config).toBeTruthy();
|
expect(config).toBeTruthy();
|
||||||
expect(config.replicationRuleEndpoint).toEqual("/api/policies/replication/testing");
|
expect(config.replicationRuleEndpoint).toEqual("/api/policies/replication/testing");
|
||||||
expect(config.replicationJobEndpoint).toEqual("/api/jobs/replication/testing");
|
expect(config.replicationBaseEndpoint).toEqual("/api/replication/testing");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,12 +9,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
ReplicationJob,
|
ReplicationJob,
|
||||||
ReplicationRule,
|
ReplicationRule,
|
||||||
ReplicationJobItem
|
ReplicationJobItem,
|
||||||
|
ReplicationTasks
|
||||||
} from "./interface";
|
} from "./interface";
|
||||||
import { RequestQueryParams } from "./RequestQueryParams";
|
import { RequestQueryParams } from "./RequestQueryParams";
|
||||||
import { map, catchError } from "rxjs/operators";
|
import { map, catchError } from "rxjs/operators";
|
||||||
import { Observable, throwError as observableThrowError } from "rxjs";
|
import { Observable, throwError as observableThrowError } from "rxjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the service methods to handle the replication (rule and job) related things.
|
* Define the service methods to handle the replication (rule and job) related things.
|
||||||
*
|
*
|
||||||
@ -57,6 +57,19 @@ export abstract class ReplicationService {
|
|||||||
ruleId: number | string
|
ruleId: number | string
|
||||||
): Observable<ReplicationRule>;
|
): Observable<ReplicationRule>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the specified replication task.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* returns {(Observable<ReplicationRule>)}
|
||||||
|
*
|
||||||
|
* @memberOf ReplicationService
|
||||||
|
*/
|
||||||
|
abstract getReplicationTasks(
|
||||||
|
executionId: number | string,
|
||||||
|
queryParams?: RequestQueryParams
|
||||||
|
): Observable<ReplicationTasks>;
|
||||||
/**
|
/**
|
||||||
* Create new replication rule.
|
* Create new replication rule.
|
||||||
*
|
*
|
||||||
@ -122,11 +135,14 @@ export abstract class ReplicationService {
|
|||||||
*/
|
*/
|
||||||
abstract disableReplicationRule(
|
abstract disableReplicationRule(
|
||||||
ruleId: number | string
|
ruleId: number | string
|
||||||
): Observable<any> ;
|
): Observable<any>;
|
||||||
|
|
||||||
abstract replicateRule(
|
abstract replicateRule(
|
||||||
ruleId: number | string
|
ruleId: number | string
|
||||||
): Observable<any> ;
|
): Observable<any>;
|
||||||
|
|
||||||
|
|
||||||
|
abstract getRegistryInfo(id: number): Observable<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the jobs for the specified replication rule.
|
* Get the jobs for the specified replication rule.
|
||||||
@ -144,11 +160,24 @@ export abstract class ReplicationService {
|
|||||||
*
|
*
|
||||||
* @memberOf ReplicationService
|
* @memberOf ReplicationService
|
||||||
*/
|
*/
|
||||||
abstract getJobs(
|
abstract getExecutions(
|
||||||
ruleId: number | string,
|
ruleId: number | string,
|
||||||
queryParams?: RequestQueryParams
|
queryParams?: RequestQueryParams
|
||||||
): Observable<ReplicationJob>;
|
): Observable<ReplicationJob>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the specified execution.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* ** deprecated param {(number | string)} endpointId
|
||||||
|
* returns {(Observable<ReplicationJob> | ReplicationJob)}
|
||||||
|
*
|
||||||
|
* @memberOf ReplicationService
|
||||||
|
*/
|
||||||
|
abstract getExecutionById(
|
||||||
|
executionId: number | string
|
||||||
|
): Observable<ReplicationJob>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the log of the specified job.
|
* Get the log of the specified job.
|
||||||
*
|
*
|
||||||
@ -178,8 +207,8 @@ export abstract class ReplicationService {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReplicationDefaultService extends ReplicationService {
|
export class ReplicationDefaultService extends ReplicationService {
|
||||||
_ruleBaseUrl: string;
|
_ruleBaseUrl: string;
|
||||||
_jobBaseUrl: string;
|
|
||||||
_replicateUrl: string;
|
_replicateUrl: string;
|
||||||
|
_baseUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: Http,
|
private http: Http,
|
||||||
@ -188,13 +217,11 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
super();
|
super();
|
||||||
this._ruleBaseUrl = config.replicationRuleEndpoint
|
this._ruleBaseUrl = config.replicationRuleEndpoint
|
||||||
? config.replicationRuleEndpoint
|
? config.replicationRuleEndpoint
|
||||||
: "/api/policies/replication";
|
: "/api/replication/policies";
|
||||||
this._jobBaseUrl = config.replicationJobEndpoint
|
|
||||||
? config.replicationJobEndpoint
|
|
||||||
: "/api/jobs/replication";
|
|
||||||
this._replicateUrl = config.replicationBaseEndpoint
|
this._replicateUrl = config.replicationBaseEndpoint
|
||||||
? config.replicationBaseEndpoint
|
? config.replicationBaseEndpoint
|
||||||
: "/api/replications";
|
: "/api/replication";
|
||||||
|
this._baseUrl = config.baseEndpoint ? config.baseEndpoint : "/api";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private methods
|
// Private methods
|
||||||
@ -205,12 +232,20 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
rule != null &&
|
rule != null &&
|
||||||
rule.name !== undefined &&
|
rule.name !== undefined &&
|
||||||
rule.name.trim() !== "" &&
|
rule.name.trim() !== "" &&
|
||||||
rule.targets.length !== 0
|
(!!rule.dest_registry || !!rule.src_registry)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRegistryInfo(id): Observable<any> {
|
||||||
|
let requestUrl: string = `${this._baseUrl}/registries/${id}/info`;
|
||||||
|
return this.http
|
||||||
|
.get(requestUrl)
|
||||||
|
.pipe(map(response => response.json())
|
||||||
|
, catchError(error => observableThrowError(error)));
|
||||||
|
}
|
||||||
|
|
||||||
public getJobBaseUrl() {
|
public getJobBaseUrl() {
|
||||||
return this._jobBaseUrl;
|
return this._replicateUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getReplicationRules(
|
public getReplicationRules(
|
||||||
@ -251,6 +286,21 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
, catchError(error => observableThrowError(error)));
|
, catchError(error => observableThrowError(error)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getReplicationTasks(
|
||||||
|
executionId: number | string,
|
||||||
|
queryParams?: RequestQueryParams
|
||||||
|
): Observable<ReplicationTasks> {
|
||||||
|
if (!executionId) {
|
||||||
|
return observableThrowError("Bad argument");
|
||||||
|
}
|
||||||
|
let url: string = `${this._replicateUrl}/executions/${executionId}/tasks`;
|
||||||
|
return this.http
|
||||||
|
.get(url,
|
||||||
|
queryParams ? buildHttpRequestOptions(queryParams) : HTTP_GET_OPTIONS)
|
||||||
|
.pipe(map(response => response.json() as ReplicationTasks)
|
||||||
|
, catchError(error => observableThrowError(error)));
|
||||||
|
}
|
||||||
|
|
||||||
public createReplicationRule(
|
public createReplicationRule(
|
||||||
replicationRule: ReplicationRule
|
replicationRule: ReplicationRule
|
||||||
): Observable<any> {
|
): Observable<any> {
|
||||||
@ -304,7 +354,7 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
return observableThrowError("Bad argument");
|
return observableThrowError("Bad argument");
|
||||||
}
|
}
|
||||||
|
|
||||||
let url: string = `${this._replicateUrl}`;
|
let url: string = `${this._replicateUrl}/executions`;
|
||||||
return this.http
|
return this.http
|
||||||
.post(url, { policy_id: ruleId }, HTTP_JSON_OPTIONS)
|
.post(url, { policy_id: ruleId }, HTTP_JSON_OPTIONS)
|
||||||
.pipe(map(response => response)
|
.pipe(map(response => response)
|
||||||
@ -340,7 +390,7 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
, catchError(error => observableThrowError(error)));
|
, catchError(error => observableThrowError(error)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getJobs(
|
public getExecutions(
|
||||||
ruleId: number | string,
|
ruleId: number | string,
|
||||||
queryParams?: RequestQueryParams
|
queryParams?: RequestQueryParams
|
||||||
): Observable<ReplicationJob> {
|
): Observable<ReplicationJob> {
|
||||||
@ -351,10 +401,45 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
if (!queryParams) {
|
if (!queryParams) {
|
||||||
queryParams = new RequestQueryParams();
|
queryParams = new RequestQueryParams();
|
||||||
}
|
}
|
||||||
|
let url: string = `${this._replicateUrl}/executions`;
|
||||||
queryParams.set("policy_id", "" + ruleId);
|
queryParams.set("policy_id", "" + ruleId);
|
||||||
return this.http
|
return this.http
|
||||||
.get(this._jobBaseUrl, buildHttpRequestOptions(queryParams))
|
.get(url, buildHttpRequestOptions(queryParams))
|
||||||
|
.pipe(map(response => {
|
||||||
|
let result: ReplicationJob = {
|
||||||
|
metadata: {
|
||||||
|
xTotalCount: 0
|
||||||
|
},
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response && response.headers) {
|
||||||
|
let xHeader: string = response.headers.get("X-Total-Count");
|
||||||
|
if (xHeader) {
|
||||||
|
result.metadata.xTotalCount = parseInt(xHeader, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.data = response.json() as ReplicationJobItem[];
|
||||||
|
if (result.metadata.xTotalCount === 0) {
|
||||||
|
if (result.data && result.data.length > 0) {
|
||||||
|
result.metadata.xTotalCount = result.data.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
, catchError(error => observableThrowError(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExecutionById(
|
||||||
|
executionId: number | string
|
||||||
|
): Observable<ReplicationJob> {
|
||||||
|
if (!executionId || executionId <= 0) {
|
||||||
|
return observableThrowError("Bad request argument.");
|
||||||
|
}
|
||||||
|
let requestUrl: string = `${this._replicateUrl}/executions/${executionId}`;
|
||||||
|
return this.http
|
||||||
|
.get(requestUrl, HTTP_GET_OPTIONS)
|
||||||
.pipe(map(response => {
|
.pipe(map(response => {
|
||||||
let result: ReplicationJob = {
|
let result: ReplicationJob = {
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -388,7 +473,7 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
return observableThrowError("Bad argument");
|
return observableThrowError("Bad argument");
|
||||||
}
|
}
|
||||||
|
|
||||||
let logUrl = `${this._jobBaseUrl}/${jobId}/log`;
|
let logUrl = `${this._replicateUrl}/${jobId}/log`;
|
||||||
return this.http
|
return this.http
|
||||||
.get(logUrl, HTTP_GET_OPTIONS)
|
.get(logUrl, HTTP_GET_OPTIONS)
|
||||||
.pipe(map(response => response.text())
|
.pipe(map(response => response.text())
|
||||||
@ -398,10 +483,14 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||||||
public stopJobs(
|
public stopJobs(
|
||||||
jobId: number | string
|
jobId: number | string
|
||||||
): Observable<any> {
|
): Observable<any> {
|
||||||
|
if (!jobId || jobId <= 0) {
|
||||||
|
return observableThrowError("Bad request argument.");
|
||||||
|
}
|
||||||
|
let requestUrl: string = `${this._replicateUrl}/executions/${jobId}`;
|
||||||
|
|
||||||
return this.http
|
return this.http
|
||||||
.put(
|
.put(
|
||||||
this._jobBaseUrl,
|
requestUrl,
|
||||||
JSON.stringify({ policy_id: jobId, status: "stop" }),
|
|
||||||
HTTP_JSON_OPTIONS
|
HTTP_JSON_OPTIONS
|
||||||
)
|
)
|
||||||
.pipe(map(response => response)
|
.pipe(map(response => response)
|
||||||
|
@ -43,7 +43,8 @@ export const enum ConfirmationTargets {
|
|||||||
CONFIG_ROUTE,
|
CONFIG_ROUTE,
|
||||||
CONFIG_TAB,
|
CONFIG_TAB,
|
||||||
HELM_CHART,
|
HELM_CHART,
|
||||||
HELM_CHART_VERSION
|
HELM_CHART_VERSION,
|
||||||
|
STOP_EXECUTIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ActionType {
|
export const enum ActionType {
|
||||||
@ -69,7 +70,7 @@ export const enum ConfirmationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const enum ConfirmationButtons {
|
export const enum ConfirmationButtons {
|
||||||
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE, REPLICATE_CANCEL
|
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE, REPLICATE_CANCEL, STOP_CANCEL
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelColor = [
|
export const LabelColor = [
|
||||||
|
@ -23,6 +23,11 @@ export const errorHandler = function (error: any): string {
|
|||||||
if (!error) {
|
if (!error) {
|
||||||
return "UNKNOWN_ERROR";
|
return "UNKNOWN_ERROR";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error && error._body) {
|
||||||
|
return error._body;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(error.statusCode || error.status)) {
|
if (!(error.statusCode || error.status)) {
|
||||||
// treat as string message
|
// treat as string message
|
||||||
return '' + error;
|
return '' + error;
|
||||||
|
@ -33,8 +33,9 @@ import { ResetPasswordComponent } from './account/password-setting/reset-passwor
|
|||||||
import { GroupComponent } from './group/group.component';
|
import { GroupComponent } from './group/group.component';
|
||||||
|
|
||||||
import { TotalReplicationPageComponent } from './replication/total-replication/total-replication-page.component';
|
import { TotalReplicationPageComponent } from './replication/total-replication/total-replication-page.component';
|
||||||
|
import { ReplicationTasksPageComponent } from './replication/replication-tasks-page/replication-tasks-page.component';
|
||||||
|
|
||||||
import { DestinationPageComponent } from './replication/destination/destination-page.component';
|
import { DestinationPageComponent } from './replication/destination/destination-page.component';
|
||||||
import { ReplicationPageComponent } from './replication/replication-page.component';
|
|
||||||
|
|
||||||
import { AuditLogComponent } from './log/audit-log.component';
|
import { AuditLogComponent } from './log/audit-log.component';
|
||||||
import { LogPageComponent } from './log/log-page.component';
|
import { LogPageComponent } from './log/log-page.component';
|
||||||
@ -109,6 +110,12 @@ const harborRoutes: Routes = [
|
|||||||
canActivate: [SystemAdminGuard],
|
canActivate: [SystemAdminGuard],
|
||||||
canActivateChild: [SystemAdminGuard],
|
canActivateChild: [SystemAdminGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'replications/:id/:tasks',
|
||||||
|
component: ReplicationTasksPageComponent,
|
||||||
|
canActivate: [SystemAdminGuard],
|
||||||
|
canActivateChild: [SystemAdminGuard],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'tags/:id/:repo',
|
path: 'tags/:id/:repo',
|
||||||
component: TagRepositoryComponent,
|
component: TagRepositoryComponent,
|
||||||
@ -170,10 +177,6 @@ const harborRoutes: Routes = [
|
|||||||
path: 'repositories/:repo/tags',
|
path: 'repositories/:repo/tags',
|
||||||
component: TagRepositoryComponent,
|
component: TagRepositoryComponent,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'replications',
|
|
||||||
component: ReplicationPageComponent,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'members',
|
path: 'members',
|
||||||
component: MemberComponent
|
component: MemberComponent
|
||||||
|
@ -13,9 +13,6 @@
|
|||||||
<li class="nav-item" *ngIf="hasMemberListPermission">
|
<li class="nav-item" *ngIf="hasMemberListPermission">
|
||||||
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
|
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *ngIf="hasReplicationListPermission">
|
|
||||||
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" *ngIf="(hasLabelListPermission && hasLabelCreatePermission) && !withAdmiral">
|
<li class="nav-item" *ngIf="(hasLabelListPermission && hasLabelCreatePermission) && !withAdmiral">
|
||||||
<a class="nav-link" routerLink="labels" routerLinkActive="active">{{'PROJECT_DETAIL.LABELS' | translate}}</a>
|
<a class="nav-link" routerLink="labels" routerLinkActive="active">{{'PROJECT_DETAIL.LABELS' | translate}}</a>
|
||||||
</li>
|
</li>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user