mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-23 17:17:46 +01:00
Merge pull request #3941 from vmware/replication_enhancement
Replication enhancement
This commit is contained in:
commit
64cc71ea12
@ -89,8 +89,8 @@ script:
|
|||||||
- sudo mkdir -p ./make/common/config/registry/
|
- sudo mkdir -p ./make/common/config/registry/
|
||||||
- sudo mv ./tests/reg_config.yml ./make/common/config/registry/config.yml
|
- sudo mv ./tests/reg_config.yml ./make/common/config/registry/config.yml
|
||||||
- sudo docker-compose -f ./make/docker-compose.test.yml up -d
|
- sudo docker-compose -f ./make/docker-compose.test.yml up -d
|
||||||
- go list ./... | grep -v -E 'vendor|tests' | xargs -L1 fgt golint
|
- go list ./... | grep -v -E 'vendor|tests|test' | xargs -L1 fgt golint
|
||||||
- go list ./... | grep -v -E 'vendor|tests' | xargs -L1 go vet
|
- go list ./... | grep -v -E 'vendor|tests|test' | xargs -L1 go vet
|
||||||
- export MYSQL_HOST=$IP
|
- export MYSQL_HOST=$IP
|
||||||
- export REGISTRY_URL=$IP:5000
|
- export REGISTRY_URL=$IP:5000
|
||||||
- echo $REGISTRY_URL
|
- echo $REGISTRY_URL
|
||||||
|
@ -1391,6 +1391,32 @@ paths:
|
|||||||
description: User need to login first.
|
description: User need to login first.
|
||||||
'500':
|
'500':
|
||||||
description: Unexpected internal errors.
|
description: Unexpected internal errors.
|
||||||
|
put:
|
||||||
|
summary: Update status of jobs. Only stop is supported for now.
|
||||||
|
description: >
|
||||||
|
The endpoint is used to stop the replication jobs of a policy.
|
||||||
|
tags:
|
||||||
|
- Products
|
||||||
|
parameters:
|
||||||
|
- name: policyinfo
|
||||||
|
in: body
|
||||||
|
description: The policy ID and status.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/UpdateJobs'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Update the status successfully.
|
||||||
|
'400':
|
||||||
|
description: Bad request because of invalid parameters.
|
||||||
|
'401':
|
||||||
|
description: User need to login first.
|
||||||
|
'403':
|
||||||
|
description: User has no privilege for the operation.
|
||||||
|
'404':
|
||||||
|
description: Resource requested does not exist.
|
||||||
|
'500':
|
||||||
|
description: Unexpected internal errors.
|
||||||
/jobs/replication/{id}:
|
/jobs/replication/{id}:
|
||||||
delete:
|
delete:
|
||||||
summary: Delete specific ID job.
|
summary: Delete specific ID job.
|
||||||
@ -1511,7 +1537,7 @@ paths:
|
|||||||
description: Create new policy.
|
description: Create new policy.
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/RepPolicyPost'
|
$ref: '#/definitions/RepPolicy'
|
||||||
tags:
|
tags:
|
||||||
- Products
|
- Products
|
||||||
responses:
|
responses:
|
||||||
@ -1566,10 +1592,10 @@ paths:
|
|||||||
description: policy ID
|
description: policy ID
|
||||||
- name: policyupdate
|
- name: policyupdate
|
||||||
in: body
|
in: body
|
||||||
description: 'Update policy name, description, target and enablement.'
|
description: 'Updated properties of the replication policy.'
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/RepPolicyUpdate'
|
$ref: '#/definitions/RepPolicy'
|
||||||
tags:
|
tags:
|
||||||
- Products
|
- Products
|
||||||
responses:
|
responses:
|
||||||
@ -1587,35 +1613,27 @@ paths:
|
|||||||
project and target.
|
project and target.
|
||||||
'500':
|
'500':
|
||||||
description: Unexpected internal errors.
|
description: Unexpected internal errors.
|
||||||
/policies/replication/{id}/enablement:
|
/replications:
|
||||||
put:
|
post:
|
||||||
summary: Put modifies enablement of the policy.
|
summary: Trigger the replication according to the specified policy.
|
||||||
description: |
|
description: |
|
||||||
This endpoint let user update policy enablement flag.
|
This endpoint is used to trigger a replication.
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: policy ID
|
||||||
in: path
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
required: true
|
|
||||||
description: policy ID
|
|
||||||
- name: enabledflag
|
|
||||||
in: body
|
in: body
|
||||||
description: The policy enablement flag.
|
description: The ID of replication policy.
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/RepPolicyEnablementReq'
|
$ref: '#/definitions/Replication'
|
||||||
tags:
|
tags:
|
||||||
- Products
|
- Products
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Update job policy enablement successfully.
|
description: Trigger the replication successfully.
|
||||||
'400':
|
|
||||||
description: Invalid enabled value.
|
|
||||||
'401':
|
'401':
|
||||||
description: User need to log in first.
|
description: User need to log in first.
|
||||||
'404':
|
'404':
|
||||||
description: The specific repository ID's policy does not exist.
|
description: The policy does not exist.
|
||||||
'500':
|
'500':
|
||||||
description: Unexpected internal errors.
|
description: Unexpected internal errors.
|
||||||
/targets:
|
/targets:
|
||||||
@ -2378,33 +2396,35 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
description: The policy ID.
|
description: The policy ID.
|
||||||
project_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: The project ID.
|
|
||||||
project_name:
|
|
||||||
type: string
|
|
||||||
description: The project name.
|
|
||||||
target_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: The target ID.
|
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: The policy name.
|
description: The policy name.
|
||||||
enabled:
|
|
||||||
type: integer
|
|
||||||
format: int
|
|
||||||
description: The policy's enabled status.
|
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
description: The description of the policy.
|
description: The description of the policy.
|
||||||
cron_str:
|
projects:
|
||||||
type: string
|
type: array
|
||||||
description: The cron string for schedule job.
|
description: The project list that the policy applys to.
|
||||||
start_time:
|
items:
|
||||||
type: string
|
$ref: '#/definitions/Project'
|
||||||
description: The start time of the policy.
|
targets:
|
||||||
|
type: array
|
||||||
|
description: The target list.
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/RepTarget'
|
||||||
|
trigger:
|
||||||
|
$ref: '#/definitions/RepTrigger'
|
||||||
|
filters:
|
||||||
|
type: array
|
||||||
|
description: The replication policy filter array.
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/RepFilter'
|
||||||
|
replicate_existing_image_now:
|
||||||
|
type: boolean
|
||||||
|
description: Whether to replicate the existing images now.
|
||||||
|
replicate_deletion:
|
||||||
|
type: boolean
|
||||||
|
description: Whether to replicate the deletion operation.
|
||||||
creation_time:
|
creation_time:
|
||||||
type: string
|
type: string
|
||||||
description: The create time of the policy.
|
description: The create time of the policy.
|
||||||
@ -2412,55 +2432,42 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
description: The update time of the policy.
|
description: The update time of the policy.
|
||||||
error_job_count:
|
error_job_count:
|
||||||
format: int
|
type: integer
|
||||||
description: The error job count number for the policy.
|
description: The error job count number for the policy.
|
||||||
deleted:
|
RepTrigger:
|
||||||
type: integer
|
|
||||||
RepPolicyPost:
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
project_id:
|
kind:
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: The project ID.
|
|
||||||
target_id:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
description: The target ID.
|
|
||||||
name:
|
|
||||||
type: string
|
type: string
|
||||||
description: The policy name.
|
description: The replication policy trigger kind. The valid values are manual, immediate and schedule.
|
||||||
enabled:
|
schedule_param:
|
||||||
type: integer
|
$ref: '#/definitions/ScheduleParam'
|
||||||
format: int
|
ScheduleParam:
|
||||||
description: '1-enable, 0-disable'
|
|
||||||
RepPolicyUpdate:
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
target_id:
|
type:
|
||||||
|
type: string
|
||||||
|
description: The schedule type. The valid values are daily and weekly.
|
||||||
|
weekday:
|
||||||
|
type: integer
|
||||||
|
format: int8
|
||||||
|
description: Optional, only used when the type is weedly. The valid values are 1-7.
|
||||||
|
offtime:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
description: The target ID.
|
description: The time offset with the UTC 00:00 in seconds.
|
||||||
name:
|
RepFilter:
|
||||||
type: string
|
|
||||||
description: The policy name.
|
|
||||||
enabled:
|
|
||||||
type: integer
|
|
||||||
format: int
|
|
||||||
description: The policy's enabled status.
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
description: The description of the policy.
|
|
||||||
cron_str:
|
|
||||||
type: string
|
|
||||||
description: The cron string for schedule job.
|
|
||||||
RepPolicyEnablementReq:
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
enabled:
|
kind:
|
||||||
type: integer
|
type: string
|
||||||
format: int
|
description: The replication policy filter kind. The valid values are project, repository and tag.
|
||||||
description: The policy enablement flag.
|
pattern:
|
||||||
|
type: string
|
||||||
|
description: The replication policy filter pattern.
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
description: This map object is the replication policy filter metadata.
|
||||||
RepTarget:
|
RepTarget:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -2947,12 +2954,24 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
description: The offest in seconds of UTC 0 o'clock, only valid when the policy type is "daily"
|
description: The offest in seconds of UTC 0 o'clock, only valid when the policy type is "daily"
|
||||||
description: The parameters of the policy, the values are dependant on the type of the policy.
|
description: The parameters of the policy, the values are dependant on the type of the policy.
|
||||||
|
Replication:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
policy_id:
|
||||||
|
type: integer
|
||||||
|
description: The ID of replication policy
|
||||||
RepositoryDescription:
|
RepositoryDescription:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
description: The description of the repository.
|
description: The description of the repository.
|
||||||
|
UpdateJobs:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
policy_id:
|
||||||
|
type: integer
|
||||||
|
description: The ID of replication policy
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: The status of jobs. The only valid value is stop for now.
|
@ -147,6 +147,8 @@ create table replication_policy (
|
|||||||
description text,
|
description text,
|
||||||
deleted tinyint (1) DEFAULT 0 NOT NULL,
|
deleted tinyint (1) DEFAULT 0 NOT NULL,
|
||||||
cron_str varchar(256),
|
cron_str varchar(256),
|
||||||
|
filters varchar(1024),
|
||||||
|
replicate_deletion tinyint (1) DEFAULT 0 NOT NULL,
|
||||||
start_time timestamp NULL,
|
start_time timestamp NULL,
|
||||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||||
@ -185,6 +187,17 @@ create table replication_job (
|
|||||||
INDEX poid_uptime (policy_id, update_time)
|
INDEX poid_uptime (policy_id, update_time)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table replication_immediate_trigger (
|
||||||
|
id int NOT NULL AUTO_INCREMENT,
|
||||||
|
policy_id int NOT NULL,
|
||||||
|
namespace varchar(256) NOT NULL,
|
||||||
|
on_push tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
on_deletion tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||||
|
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
create table img_scan_job (
|
create table img_scan_job (
|
||||||
id int NOT NULL AUTO_INCREMENT,
|
id int NOT NULL AUTO_INCREMENT,
|
||||||
status varchar(64) NOT NULL,
|
status varchar(64) NOT NULL,
|
||||||
|
@ -142,6 +142,8 @@ create table replication_policy (
|
|||||||
description text,
|
description text,
|
||||||
deleted tinyint (1) DEFAULT 0 NOT NULL,
|
deleted tinyint (1) DEFAULT 0 NOT NULL,
|
||||||
cron_str varchar(256),
|
cron_str varchar(256),
|
||||||
|
filters varchar(1024),
|
||||||
|
replicate_deletion tinyint (1) DEFAULT 0 NOT NULL,
|
||||||
start_time timestamp NULL,
|
start_time timestamp NULL,
|
||||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||||
update_time timestamp default CURRENT_TIMESTAMP
|
update_time timestamp default CURRENT_TIMESTAMP
|
||||||
@ -175,6 +177,16 @@ create table replication_job (
|
|||||||
update_time timestamp default CURRENT_TIMESTAMP
|
update_time timestamp default CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table replication_immediate_trigger (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
policy_id int NOT NULL,
|
||||||
|
namespace varchar(256) NOT NULL,
|
||||||
|
on_push tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
on_deletion tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||||
|
update_time timestamp default CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
create table img_scan_job (
|
create table img_scan_job (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
|
@ -15,16 +15,11 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/vmware/harbor/src/adminserver/client/auth"
|
|
||||||
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
|
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
|
||||||
|
"github.com/vmware/harbor/src/common/http"
|
||||||
|
"github.com/vmware/harbor/src/common/http/modifier/auth"
|
||||||
"github.com/vmware/harbor/src/common/utils"
|
"github.com/vmware/harbor/src/common/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,38 +38,29 @@ type Client interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewClient return an instance of Adminserver client
|
// NewClient return an instance of Adminserver client
|
||||||
func NewClient(baseURL string, authorizer auth.Authorizer) Client {
|
func NewClient(baseURL string, cfg *Config) Client {
|
||||||
baseURL = strings.TrimRight(baseURL, "/")
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
if !strings.Contains(baseURL, "://") {
|
if !strings.Contains(baseURL, "://") {
|
||||||
baseURL = "http://" + baseURL
|
baseURL = "http://" + baseURL
|
||||||
}
|
}
|
||||||
return &client{
|
client := &client{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
client: &http.Client{},
|
|
||||||
authorizer: authorizer,
|
|
||||||
}
|
}
|
||||||
|
if cfg != nil {
|
||||||
|
authorizer := auth.NewSecretAuthorizer(cfg.Secret)
|
||||||
|
client.client = http.NewClient(nil, authorizer)
|
||||||
|
}
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
client *http.Client
|
client *http.Client
|
||||||
authorizer auth.Authorizer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// do creates request and authorizes it if authorizer is not nil
|
// Config contains configurations needed for client
|
||||||
func (c *client) do(method, relativePath string, body io.Reader) (*http.Response, error) {
|
type Config struct {
|
||||||
url := c.baseURL + relativePath
|
Secret string
|
||||||
req, err := http.NewRequest(method, url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.authorizer != nil {
|
|
||||||
if err := c.authorizer.Authorize(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.client.Do(req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) Ping() error {
|
func (c *client) Ping() error {
|
||||||
@ -88,96 +74,32 @@ func (c *client) Ping() error {
|
|||||||
|
|
||||||
// GetCfgs ...
|
// GetCfgs ...
|
||||||
func (c *client) GetCfgs() (map[string]interface{}, error) {
|
func (c *client) GetCfgs() (map[string]interface{}, error) {
|
||||||
resp, err := c.do(http.MethodGet, "/api/configurations", nil)
|
url := c.baseURL + "/api/configurations"
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("failed to get configurations: %d %s",
|
|
||||||
resp.StatusCode, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfgs := map[string]interface{}{}
|
cfgs := map[string]interface{}{}
|
||||||
if err = json.Unmarshal(b, &cfgs); err != nil {
|
if err := c.client.Get(url, &cfgs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfgs, nil
|
return cfgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCfgs ...
|
// UpdateCfgs ...
|
||||||
func (c *client) UpdateCfgs(cfgs map[string]interface{}) error {
|
func (c *client) UpdateCfgs(cfgs map[string]interface{}) error {
|
||||||
data, err := json.Marshal(cfgs)
|
url := c.baseURL + "/api/configurations"
|
||||||
if err != nil {
|
return c.client.Put(url, cfgs)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.do(http.MethodPut, "/api/configurations", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to update configurations: %d %s",
|
|
||||||
resp.StatusCode, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetCfgs ...
|
// ResetCfgs ...
|
||||||
func (c *client) ResetCfgs() error {
|
func (c *client) ResetCfgs() error {
|
||||||
resp, err := c.do(http.MethodPost, "/api/configurations/reset", nil)
|
url := c.baseURL + "/api/configurations/reset"
|
||||||
if err != nil {
|
return c.client.Post(url)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to reset configurations: %d %s",
|
|
||||||
resp.StatusCode, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capacity ...
|
// Capacity ...
|
||||||
func (c *client) Capacity() (*imagestorage.Capacity, error) {
|
func (c *client) Capacity() (*imagestorage.Capacity, error) {
|
||||||
resp, err := c.do(http.MethodGet, "/api/systeminfo/capacity", nil)
|
url := c.baseURL + "/api/systeminfo/capacity"
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("failed to get capacity: %d %s",
|
|
||||||
resp.StatusCode, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
capacity := &imagestorage.Capacity{}
|
capacity := &imagestorage.Capacity{}
|
||||||
if err = json.Unmarshal(b, capacity); err != nil {
|
if err := c.client.Get(url, capacity); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return capacity, nil
|
return capacity, nil
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
c = NewClient(server.URL, nil)
|
c = NewClient(server.URL, &Config{})
|
||||||
|
|
||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
@ -941,7 +941,6 @@ func TestFilterRepTargets(t *testing.T) {
|
|||||||
func TestAddRepPolicy(t *testing.T) {
|
func TestAddRepPolicy(t *testing.T) {
|
||||||
policy := models.RepPolicy{
|
policy := models.RepPolicy{
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
Enabled: 1,
|
|
||||||
TargetID: targetID,
|
TargetID: targetID,
|
||||||
Description: "whatever",
|
Description: "whatever",
|
||||||
Name: "mypolicy",
|
Name: "mypolicy",
|
||||||
@ -961,15 +960,10 @@ func TestAddRepPolicy(t *testing.T) {
|
|||||||
t.Errorf("Unable to find a policy with id: %d", id)
|
t.Errorf("Unable to find a policy with id: %d", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Name != "mypolicy" || p.TargetID != targetID || p.Enabled != 1 || p.Description != "whatever" {
|
if p.Name != "mypolicy" || p.TargetID != targetID || p.Description != "whatever" {
|
||||||
t.Errorf("The data does not match, expected: Name: mypolicy, TargetID: %d, Enabled: 1, Description: whatever;\n result: Name: %s, TargetID: %d, Enabled: %d, Description: %s",
|
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.Enabled, p.Description)
|
targetID, p.Name, p.TargetID, p.Description)
|
||||||
}
|
}
|
||||||
var tm = time.Now().AddDate(0, 0, -1)
|
|
||||||
if !p.StartTime.After(tm) {
|
|
||||||
t.Errorf("Unexpected start_time: %v", p.StartTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRepPolicyByTarget(t *testing.T) {
|
func TestGetRepPolicyByTarget(t *testing.T) {
|
||||||
@ -1019,44 +1013,9 @@ func TestGetRepPolicyByName(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDisableRepPolicy(t *testing.T) {
|
|
||||||
err := DisableRepPolicy(policyID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to disable policy, id: %d", policyID)
|
|
||||||
}
|
|
||||||
p, err := GetRepPolicy(policyID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetPolicy: %v, id: %d", err, policyID)
|
|
||||||
}
|
|
||||||
if p == nil {
|
|
||||||
t.Errorf("Unable to find a policy with id: %d", policyID)
|
|
||||||
}
|
|
||||||
if p.Enabled == 1 {
|
|
||||||
t.Errorf("The Enabled value of replication policy is still 1 after disabled, id: %d", policyID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnableRepPolicy(t *testing.T) {
|
|
||||||
err := EnableRepPolicy(policyID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to disable policy, id: %d", policyID)
|
|
||||||
}
|
|
||||||
p, err := GetRepPolicy(policyID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error occurred in GetPolicy: %v, id: %d", err, policyID)
|
|
||||||
}
|
|
||||||
if p == nil {
|
|
||||||
t.Errorf("Unable to find a policy with id: %d", policyID)
|
|
||||||
}
|
|
||||||
if p.Enabled == 0 {
|
|
||||||
t.Errorf("The Enabled value of replication policy is still 0 after disabled, id: %d", policyID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddRepPolicy2(t *testing.T) {
|
func TestAddRepPolicy2(t *testing.T) {
|
||||||
policy2 := models.RepPolicy{
|
policy2 := models.RepPolicy{
|
||||||
ProjectID: 3,
|
ProjectID: 3,
|
||||||
Enabled: 0,
|
|
||||||
TargetID: 3,
|
TargetID: 3,
|
||||||
Description: "whatever",
|
Description: "whatever",
|
||||||
Name: "mypolicy",
|
Name: "mypolicy",
|
||||||
@ -1073,10 +1032,6 @@ func TestAddRepPolicy2(t *testing.T) {
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
t.Errorf("Unable to find a policy with id: %d", policyID2)
|
t.Errorf("Unable to find a policy with id: %d", policyID2)
|
||||||
}
|
}
|
||||||
var tm time.Time
|
|
||||||
if p.StartTime.After(tm) {
|
|
||||||
t.Errorf("Unexpected start_time: %v", p.StartTime)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddRepJob(t *testing.T) {
|
func TestAddRepJob(t *testing.T) {
|
||||||
|
@ -106,34 +106,26 @@ func FilterRepTargets(name string) ([]*models.RepTarget, error) {
|
|||||||
// AddRepPolicy ...
|
// AddRepPolicy ...
|
||||||
func AddRepPolicy(policy models.RepPolicy) (int64, error) {
|
func AddRepPolicy(policy models.RepPolicy) (int64, error) {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
sql := `insert into replication_policy (name, project_id, target_id, enabled, description, cron_str, start_time, creation_time, update_time ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
sql := `insert into replication_policy (name, project_id, target_id, enabled, description, cron_str, creation_time, update_time, filters, replicate_deletion)
|
||||||
p, err := o.Raw(sql).Prepare()
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
params := []interface{}{}
|
params := []interface{}{}
|
||||||
params = append(params, policy.Name, policy.ProjectID, policy.TargetID, policy.Enabled, policy.Description, policy.CronStr)
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if policy.Enabled == 1 {
|
params = append(params, policy.Name, policy.ProjectID, policy.TargetID, 1,
|
||||||
params = append(params, now)
|
policy.Description, policy.Trigger, now, now, policy.Filters,
|
||||||
} else {
|
policy.ReplicateDeletion)
|
||||||
params = append(params, nil)
|
|
||||||
}
|
|
||||||
params = append(params, now, now)
|
|
||||||
|
|
||||||
r, err := p.Exec(params...)
|
result, err := o.Raw(sql, params...).Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
id, err := r.LastInsertId()
|
|
||||||
return id, err
|
return result.LastInsertId()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepPolicy ...
|
// GetRepPolicy ...
|
||||||
func GetRepPolicy(id int64) (*models.RepPolicy, error) {
|
func GetRepPolicy(id int64) (*models.RepPolicy, error) {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
sql := `select * from replication_policy where id = ?`
|
sql := `select * from replication_policy where id = ? and deleted = 0`
|
||||||
|
|
||||||
var policy models.RepPolicy
|
var policy models.RepPolicy
|
||||||
|
|
||||||
@ -154,8 +146,9 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error
|
|||||||
var args []interface{}
|
var args []interface{}
|
||||||
|
|
||||||
sql := `select rp.id, rp.project_id, rp.target_id,
|
sql := `select rp.id, rp.project_id, rp.target_id,
|
||||||
rt.name as target_name, rp.name, rp.enabled, rp.description,
|
rt.name as target_name, rp.name, rp.description,
|
||||||
rp.cron_str, rp.start_time, rp.creation_time, rp.update_time,
|
rp.cron_str, rp.filters, rp.replicate_deletion,
|
||||||
|
rp.creation_time, rp.update_time,
|
||||||
count(rj.status) as error_job_count
|
count(rj.status) as error_job_count
|
||||||
from replication_policy rp
|
from replication_policy rp
|
||||||
left join replication_target rt on rp.target_id=rt.id
|
left join replication_target rt on rp.target_id=rt.id
|
||||||
@ -181,6 +174,7 @@ func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error
|
|||||||
if _, err := o.Raw(sql, args).QueryRows(&policies); err != nil {
|
if _, err := o.Raw(sql, args).QueryRows(&policies); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return policies, nil
|
return policies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +241,8 @@ func GetRepPolicyByProjectAndTarget(projectID, targetID int64) ([]*models.RepPol
|
|||||||
func UpdateRepPolicy(policy *models.RepPolicy) error {
|
func UpdateRepPolicy(policy *models.RepPolicy) error {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
policy.UpdateTime = time.Now()
|
policy.UpdateTime = time.Now()
|
||||||
_, err := o.Update(policy, "TargetID", "Name", "Enabled", "Description", "CronStr", "UpdateTime")
|
_, err := o.Update(policy, "ProjectID", "TargetID", "Name", "Description",
|
||||||
|
"Trigger", "Filters", "ReplicateDeletion", "UpdateTime")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,36 +258,6 @@ func DeleteRepPolicy(id int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRepPolicyEnablement ...
|
|
||||||
func UpdateRepPolicyEnablement(id int64, enabled int) error {
|
|
||||||
o := GetOrmer()
|
|
||||||
p := models.RepPolicy{
|
|
||||||
ID: id,
|
|
||||||
Enabled: enabled,
|
|
||||||
UpdateTime: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if enabled == 1 {
|
|
||||||
p.StartTime = time.Now()
|
|
||||||
_, err = o.Update(&p, "Enabled", "StartTime")
|
|
||||||
} else {
|
|
||||||
_, err = o.Update(&p, "Enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableRepPolicy ...
|
|
||||||
func EnableRepPolicy(id int64) error {
|
|
||||||
return UpdateRepPolicyEnablement(id, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisableRepPolicy ...
|
|
||||||
func DisableRepPolicy(id int64) error {
|
|
||||||
return UpdateRepPolicyEnablement(id, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRepJob ...
|
// AddRepJob ...
|
||||||
func AddRepJob(job models.RepJob) (int64, error) {
|
func AddRepJob(job models.RepJob) (int64, error) {
|
||||||
o := GetOrmer()
|
o := GetOrmer()
|
||||||
|
62
src/common/dao/watch_item.go
Normal file
62
src/common/dao/watch_item.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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/vmware/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) {
|
||||||
|
now := time.Now()
|
||||||
|
item.CreationTime = now
|
||||||
|
item.UpdateTime = now
|
||||||
|
return GetOrmer().Insert(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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", 1)
|
||||||
|
} else if operation == "delete" {
|
||||||
|
qs = qs.Filter("OnDeletion", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []models.WatchItem{}
|
||||||
|
_, err := qs.All(&items)
|
||||||
|
return items, err
|
||||||
|
}
|
71
src/common/dao/watch_item_test.go
Normal file
71
src/common/dao/watch_item_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
162
src/common/http/client.go
Normal file
162
src/common/http/client.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/http/modifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is a util for common HTTP operations, such Get, Head, Post, Put and Delete.
|
||||||
|
// Use Do instead if those methods can not meet your requirement
|
||||||
|
type Client struct {
|
||||||
|
modifiers []modifier.Modifier
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates an instance of Client.
|
||||||
|
// Use net/http.Client as the default value if c is nil.
|
||||||
|
// Modifiers modify the request before sending it.
|
||||||
|
func NewClient(c *http.Client, modifiers ...modifier.Modifier) *Client {
|
||||||
|
client := &Client{
|
||||||
|
client: c,
|
||||||
|
}
|
||||||
|
if client.client == nil {
|
||||||
|
client.client = &http.Client{}
|
||||||
|
}
|
||||||
|
if len(modifiers) > 0 {
|
||||||
|
client.modifiers = modifiers
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do ...
|
||||||
|
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
for _, modifier := range c.modifiers {
|
||||||
|
if err := modifier.Modify(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ...
|
||||||
|
func (c *Client) Get(url string, v ...interface{}) error {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := c.do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(data, v[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head ...
|
||||||
|
func (c *Client) Head(url string) error {
|
||||||
|
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = c.do(req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post ...
|
||||||
|
func (c *Client) Post(url string, v ...interface{}) error {
|
||||||
|
var reader io.Reader
|
||||||
|
if len(v) > 0 {
|
||||||
|
data, err := json.Marshal(v[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
_, err = c.do(req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put ...
|
||||||
|
func (c *Client) Put(url string, v ...interface{}) error {
|
||||||
|
var reader io.Reader
|
||||||
|
if len(v) > 0 {
|
||||||
|
data := []byte{}
|
||||||
|
data, err := json.Marshal(v[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, url, reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
_, err = c.do(req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete ...
|
||||||
|
func (c *Client) Delete(url string) error {
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = c.do(req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) do(req *http.Request) ([]byte, error) {
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||||
|
return nil, &Error{
|
||||||
|
Code: resp.StatusCode,
|
||||||
|
Message: string(data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
30
src/common/http/error.go
Normal file
30
src/common/http/error.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error wrap HTTP status code and message as an error
|
||||||
|
type Error struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error ...
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("http error: code %d, message %s", e.Code, e.Message)
|
||||||
|
}
|
54
src/common/http/modifier/auth/auth.go
Normal file
54
src/common/http/modifier/auth/auth.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/http/modifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
secretCookieName = "secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorizer is a kind of Modifier used to authorize the requests
|
||||||
|
type Authorizer modifier.Modifier
|
||||||
|
|
||||||
|
// SecretAuthorizer authorizes the requests with the specified secret
|
||||||
|
type SecretAuthorizer struct {
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecretAuthorizer returns an instance of SecretAuthorizer
|
||||||
|
func NewSecretAuthorizer(secret string) *SecretAuthorizer {
|
||||||
|
return &SecretAuthorizer{
|
||||||
|
secret: secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the request by adding secret authentication information
|
||||||
|
func (s *SecretAuthorizer) Modify(req *http.Request) error {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("the request is null")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: secretCookieName,
|
||||||
|
Value: s.secret,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@ -19,25 +18,22 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAuthorize(t *testing.T) {
|
func TestAuthorizeOfSecretAuthorizer(t *testing.T) {
|
||||||
cookieName := "secret"
|
|
||||||
secret := "secret"
|
secret := "secret"
|
||||||
authorizer := NewSecretAuthorizer(cookieName, secret)
|
authorizer := NewSecretAuthorizer(secret)
|
||||||
|
|
||||||
|
// nil request
|
||||||
|
require.NotNil(t, authorizer.Modify(nil))
|
||||||
|
|
||||||
|
// valid request
|
||||||
req, err := http.NewRequest("", "", nil)
|
req, err := http.NewRequest("", "", nil)
|
||||||
if !assert.Nil(t, err, "unexpected error") {
|
require.Nil(t, err)
|
||||||
return
|
require.Nil(t, authorizer.Modify(req))
|
||||||
}
|
require.Equal(t, 1, len(req.Cookies()))
|
||||||
|
v, err := req.Cookie(secretCookieName)
|
||||||
err = authorizer.Authorize(req)
|
require.Nil(t, err)
|
||||||
if !assert.Nil(t, err, "unexpected error") {
|
assert.Equal(t, secret, v.Value)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie, err := req.Cookie(cookieName)
|
|
||||||
if !assert.Nil(t, err, "unexpected error") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, secret, cookie.Value, "unexpected cookie")
|
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package registry
|
package modifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
@ -30,6 +30,7 @@ func init() {
|
|||||||
new(RepoRecord),
|
new(RepoRecord),
|
||||||
new(ImgScanOverview),
|
new(ImgScanOverview),
|
||||||
new(ClairVulnTimestamp),
|
new(ClairVulnTimestamp),
|
||||||
|
new(WatchItem),
|
||||||
new(ProjectMetadata),
|
new(ProjectMetadata),
|
||||||
new(ConfigEntry))
|
new(ConfigEntry))
|
||||||
}
|
}
|
||||||
|
@ -38,48 +38,17 @@ const (
|
|||||||
|
|
||||||
// RepPolicy is the model for a replication policy, which associate to a project and a target (destination)
|
// RepPolicy is the model for a replication policy, which associate to a project and a target (destination)
|
||||||
type RepPolicy struct {
|
type RepPolicy struct {
|
||||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
ID int64 `orm:"pk;auto;column(id)"`
|
||||||
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
|
ProjectID int64 `orm:"column(project_id)" `
|
||||||
ProjectName string `json:"project_name,omitempty"`
|
TargetID int64 `orm:"column(target_id)"`
|
||||||
TargetID int64 `orm:"column(target_id)" json:"target_id"`
|
Name string `orm:"column(name)"`
|
||||||
TargetName string `json:"target_name,omitempty"`
|
Description string `orm:"column(description)"`
|
||||||
Name string `orm:"column(name)" json:"name"`
|
Trigger string `orm:"column(cron_str)"`
|
||||||
// Target RepTarget `orm:"-" json:"target"`
|
Filters string `orm:"column(filters)"`
|
||||||
Enabled int `orm:"column(enabled)" json:"enabled"`
|
ReplicateDeletion bool `orm:"column(replicate_deletion)"`
|
||||||
Description string `orm:"column(description)" json:"description"`
|
CreationTime time.Time `orm:"column(creation_time);auto_now_add"`
|
||||||
CronStr string `orm:"column(cron_str)" json:"cron_str"`
|
UpdateTime time.Time `orm:"column(update_time);auto_now"`
|
||||||
StartTime time.Time `orm:"column(start_time)" json:"start_time"`
|
Deleted int `orm:"column(deleted)"`
|
||||||
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"`
|
|
||||||
ErrorJobCount int `json:"error_job_count"`
|
|
||||||
Deleted int `orm:"column(deleted)" json:"deleted"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid ...
|
|
||||||
func (r *RepPolicy) 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 r.ProjectID <= 0 {
|
|
||||||
v.SetError("project_id", "invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.TargetID <= 0 {
|
|
||||||
v.SetError("target_id", "invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Enabled != 0 && r.Enabled != 1 {
|
|
||||||
v.SetError("enabled", "must be 0 or 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.CronStr) > 256 {
|
|
||||||
v.SetError("cron_str", "max length is 256")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepJob is the model for a replication job, which is the execution unit on job service, currently it is used to transfer/remove
|
// RepJob is the model for a replication job, which is the execution unit on job service, currently it is used to transfer/remove
|
||||||
|
35
src/common/models/watch_item.go
Normal file
35
src/common/models/watch_item.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WatchItem ...
|
||||||
|
type WatchItem struct {
|
||||||
|
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||||
|
PolicyID int64 `orm:"column(policy_id)" json:"policy_id"`
|
||||||
|
Namespace string `orm:"column(namespace)" json:"namespace"`
|
||||||
|
OnDeletion bool `orm:"column(on_deletion)" json:"on_deletion"`
|
||||||
|
OnPush bool `orm:"column(on_push)" json:"on_push"`
|
||||||
|
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
|
||||||
|
UpdateTime time.Time `orm:"column(update_time)" json:"update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//TableName ...
|
||||||
|
func (w *WatchItem) TableName() string {
|
||||||
|
return "replication_immediate_trigger"
|
||||||
|
}
|
@ -39,6 +39,7 @@ func (fsh *fakeStatelessHandler) Handle(v interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSubscribeAndUnSubscribe(t *testing.T) {
|
func TestSubscribeAndUnSubscribe(t *testing.T) {
|
||||||
|
count := len(notificationWatcher.handlers)
|
||||||
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -59,7 +60,7 @@ func TestSubscribeAndUnSubscribe(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(notificationWatcher.handlers) != 2 {
|
if len(notificationWatcher.handlers) != (count + 2) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ func TestSubscribeAndUnSubscribe(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(notificationWatcher.handlers) != 1 {
|
if len(notificationWatcher.handlers) != (count + 1) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,12 +104,13 @@ func TestSubscribeAndUnSubscribe(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(notificationWatcher.handlers) != 0 {
|
if len(notificationWatcher.handlers) != count {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublish(t *testing.T) {
|
func TestPublish(t *testing.T) {
|
||||||
|
count := len(notificationWatcher.handlers)
|
||||||
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -119,7 +121,7 @@ func TestPublish(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(notificationWatcher.handlers) != 2 {
|
if len(notificationWatcher.handlers) != (count + 2) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,12 +151,13 @@ func TestPublish(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConcurrentPublish(t *testing.T) {
|
func TestConcurrentPublish(t *testing.T) {
|
||||||
|
count := len(notificationWatcher.handlers)
|
||||||
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(notificationWatcher.handlers) != 1 {
|
if len(notificationWatcher.handlers) != (count + 1) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,11 +189,12 @@ func TestConcurrentPublishWithScanPolicyHandler(t *testing.T) {
|
|||||||
t.Fatal("Policy scheduler is not started")
|
t.Fatal("Policy scheduler is not started")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
count := len(notificationWatcher.handlers)
|
||||||
if err := Subscribe("testing_topic", &ScanPolicyNotificationHandler{}); err != nil {
|
if err := Subscribe("testing_topic", &ScanPolicyNotificationHandler{}); err != nil {
|
||||||
t.Fatal(err.Error())
|
t.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
if len(notificationWatcher.handlers) != 1 {
|
if len(notificationWatcher.handlers) != (count + 1) {
|
||||||
t.Fatal("Handler is not registered")
|
t.Fatalf("Handler is not registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
utcTime := time.Now().UTC().Unix()
|
utcTime := time.Now().UTC().Unix()
|
||||||
@ -209,7 +213,7 @@ func TestConcurrentPublishWithScanPolicyHandler(t *testing.T) {
|
|||||||
t.Fatal(err.Error())
|
t.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(notificationWatcher.handlers) != 0 {
|
if len(notificationWatcher.handlers) != count {
|
||||||
t.Fatal("Handler is not unregistered")
|
t.Fatal("Handler is not unregistered")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ func (s *ScanPolicyNotificationHandler) Handle(value interface{}) error {
|
|||||||
|
|
||||||
//To check and compare if the related parameter is changed.
|
//To check and compare if the related parameter is changed.
|
||||||
if pl := scheduler.DefaultScheduler.GetPolicy(alternatePolicy); pl != nil {
|
if pl := scheduler.DefaultScheduler.GetPolicy(alternatePolicy); pl != nil {
|
||||||
policyCandidate := policy.NewAlternatePolicy(&policy.AlternatePolicyConfiguration{
|
policyCandidate := policy.NewAlternatePolicy(alternatePolicy, &policy.AlternatePolicyConfiguration{
|
||||||
Duration: 24 * time.Hour,
|
Duration: 24 * time.Hour,
|
||||||
OffsetTime: notification.DailyTime,
|
OffsetTime: notification.DailyTime,
|
||||||
})
|
})
|
||||||
@ -95,7 +95,7 @@ func (s *ScanPolicyNotificationHandler) Handle(value interface{}) error {
|
|||||||
|
|
||||||
//Schedule policy.
|
//Schedule policy.
|
||||||
func schedulePolicy(notification ScanPolicyNotification) error {
|
func schedulePolicy(notification ScanPolicyNotification) error {
|
||||||
schedulePolicy := policy.NewAlternatePolicy(&policy.AlternatePolicyConfiguration{
|
schedulePolicy := policy.NewAlternatePolicy(alternatePolicy, &policy.AlternatePolicyConfiguration{
|
||||||
Duration: 24 * time.Hour,
|
Duration: 24 * time.Hour,
|
||||||
OffsetTime: notification.DailyTime,
|
OffsetTime: notification.DailyTime,
|
||||||
})
|
})
|
||||||
|
@ -10,11 +10,22 @@ import (
|
|||||||
"github.com/vmware/harbor/src/common/utils/log"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
oneDay = 24 * 3600
|
||||||
|
)
|
||||||
|
|
||||||
//AlternatePolicyConfiguration store the related configurations for alternate policy.
|
//AlternatePolicyConfiguration store the related configurations for alternate policy.
|
||||||
type AlternatePolicyConfiguration struct {
|
type AlternatePolicyConfiguration struct {
|
||||||
//Duration is the interval of executing attached tasks.
|
//Duration is the interval of executing attached tasks.
|
||||||
|
//E.g: 24*3600 for daily
|
||||||
|
// 7*24*3600 for weekly
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
|
|
||||||
|
//An integer to indicate the the weekday of the week. Please be noted that Sunday is 7.
|
||||||
|
//Use default value 0 to indicate weekday is not set.
|
||||||
|
//To support by weekly function.
|
||||||
|
Weekday int8
|
||||||
|
|
||||||
//OffsetTime is the execution time point of each turn
|
//OffsetTime is the execution time point of each turn
|
||||||
//It's a number to indicate the seconds offset to the 00:00 of UTC time.
|
//It's a number to indicate the seconds offset to the 00:00 of UTC time.
|
||||||
OffsetTime int64
|
OffsetTime int64
|
||||||
@ -42,16 +53,21 @@ type AlternatePolicy struct {
|
|||||||
|
|
||||||
//Channel used to receive terminate signal.
|
//Channel used to receive terminate signal.
|
||||||
terminator chan bool
|
terminator chan bool
|
||||||
|
|
||||||
|
//Unique name of this policy to support multiple instances
|
||||||
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewAlternatePolicy is constructor of creating AlternatePolicy.
|
//NewAlternatePolicy is constructor of creating AlternatePolicy.
|
||||||
func NewAlternatePolicy(config *AlternatePolicyConfiguration) *AlternatePolicy {
|
//Accept name and configuration as parameters.
|
||||||
|
func NewAlternatePolicy(name string, config *AlternatePolicyConfiguration) *AlternatePolicy {
|
||||||
return &AlternatePolicy{
|
return &AlternatePolicy{
|
||||||
RWMutex: new(sync.RWMutex),
|
RWMutex: new(sync.RWMutex),
|
||||||
tasks: task.NewDefaultStore(),
|
tasks: task.NewDefaultStore(),
|
||||||
config: config,
|
config: config,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
terminator: make(chan bool),
|
terminator: make(chan bool),
|
||||||
|
name: name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +78,7 @@ func (alp *AlternatePolicy) GetConfig() *AlternatePolicyConfiguration {
|
|||||||
|
|
||||||
//Name is an implementation of same method in policy interface.
|
//Name is an implementation of same method in policy interface.
|
||||||
func (alp *AlternatePolicy) Name() string {
|
func (alp *AlternatePolicy) Name() string {
|
||||||
return "Alternate Policy"
|
return alp.name
|
||||||
}
|
}
|
||||||
|
|
||||||
//Tasks is an implementation of same method in policy interface.
|
//Tasks is an implementation of same method in policy interface.
|
||||||
@ -110,6 +126,11 @@ func (alp *AlternatePolicy) Evaluate() (<-chan bool, error) {
|
|||||||
defer alp.Unlock()
|
defer alp.Unlock()
|
||||||
alp.Lock()
|
alp.Lock()
|
||||||
|
|
||||||
|
//Check if configuration is valid
|
||||||
|
if !alp.isValidConfig() {
|
||||||
|
return nil, errors.New("Policy configuration is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
//Check if policy instance is still running
|
//Check if policy instance is still running
|
||||||
if alp.isEnabled {
|
if alp.isEnabled {
|
||||||
return nil, fmt.Errorf("Instance of policy %s is still running", alp.Name())
|
return nil, fmt.Errorf("Instance of policy %s is still running", alp.Name())
|
||||||
@ -124,19 +145,41 @@ func (alp *AlternatePolicy) Evaluate() (<-chan bool, error) {
|
|||||||
alp.evaluation = make(chan bool)
|
alp.evaluation = make(chan bool)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
var (
|
||||||
|
waitingTime int64
|
||||||
|
)
|
||||||
timeNow := time.Now().UTC()
|
timeNow := time.Now().UTC()
|
||||||
|
|
||||||
//Reach the execution time point?
|
//Reach the execution time point?
|
||||||
|
//Weekday is set
|
||||||
|
if alp.config.Weekday > 0 {
|
||||||
|
targetWeekday := (alp.config.Weekday + 7) % 7
|
||||||
|
currentWeekday := timeNow.Weekday()
|
||||||
|
weekdayDiff := (int)(targetWeekday - (int8)(currentWeekday))
|
||||||
|
if weekdayDiff < 0 {
|
||||||
|
weekdayDiff += 7
|
||||||
|
}
|
||||||
|
waitingTime = (int64)(weekdayDiff * oneDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Time
|
||||||
utcTime := (int64)(timeNow.Hour()*3600 + timeNow.Minute()*60)
|
utcTime := (int64)(timeNow.Hour()*3600 + timeNow.Minute()*60)
|
||||||
diff := alp.config.OffsetTime - utcTime
|
diff := alp.config.OffsetTime - utcTime
|
||||||
if diff < 0 {
|
if waitingTime > 0 {
|
||||||
diff += 24 * 3600
|
waitingTime += diff
|
||||||
|
} else {
|
||||||
|
waitingTime = diff
|
||||||
|
if waitingTime < 0 {
|
||||||
|
waitingTime += oneDay
|
||||||
}
|
}
|
||||||
if diff > 0 {
|
}
|
||||||
|
|
||||||
|
//Let's wait for a while
|
||||||
|
if waitingTime > 0 {
|
||||||
//Wait for a while.
|
//Wait for a while.
|
||||||
log.Infof("Waiting for %d seconds after comparing offset %d and utc time %d\n", diff, alp.config.OffsetTime, utcTime)
|
log.Infof("Waiting for %d seconds after comparing offset %d and utc time %d\n", diff, alp.config.OffsetTime, utcTime)
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Duration(diff) * time.Second):
|
case <-time.After(time.Duration(waitingTime) * time.Second):
|
||||||
case <-alp.terminator:
|
case <-alp.terminator:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -188,7 +231,10 @@ func (alp *AlternatePolicy) Equal(p Policy) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg == nil || (cfg.Duration == cfg2.Duration && cfg.OffsetTime == cfg2.OffsetTime)
|
return cfg == nil ||
|
||||||
|
(cfg.Duration == cfg2.Duration &&
|
||||||
|
cfg.OffsetTime == cfg2.OffsetTime &&
|
||||||
|
cfg.Weekday == cfg2.Weekday)
|
||||||
}
|
}
|
||||||
|
|
||||||
//IsEnabled is an implementation of same method in policy interface.
|
//IsEnabled is an implementation of same method in policy interface.
|
||||||
@ -198,3 +244,8 @@ func (alp *AlternatePolicy) IsEnabled() bool {
|
|||||||
|
|
||||||
return alp.isEnabled
|
return alp.isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check if the config is valid. At least it should have the configurations for supporting daily policy.
|
||||||
|
func (alp *AlternatePolicy) isValidConfig() bool {
|
||||||
|
return alp.config != nil && alp.config.Duration > 0 && alp.config.OffsetTime >= 0
|
||||||
|
}
|
||||||
|
@ -6,6 +6,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testPolicyName = "TestingPolicy"
|
||||||
|
)
|
||||||
|
|
||||||
type fakeTask struct {
|
type fakeTask struct {
|
||||||
number int32
|
number int32
|
||||||
}
|
}
|
||||||
@ -24,18 +28,18 @@ func (ft *fakeTask) Number() int32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
func TestBasic(t *testing.T) {
|
||||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{})
|
tp := NewAlternatePolicy(testPolicyName, &AlternatePolicyConfiguration{})
|
||||||
err := tp.AttachTasks(&fakeTask{number: 100})
|
err := tp.AttachTasks(&fakeTask{number: 100})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
if tp.GetConfig() == nil {
|
if tp.GetConfig() == nil {
|
||||||
t.Fail()
|
t.Fatal("nil config")
|
||||||
}
|
}
|
||||||
|
|
||||||
if tp.Name() != "Alternate Policy" {
|
if tp.Name() != testPolicyName {
|
||||||
t.Fail()
|
t.Fatalf("Wrong name %s", tp.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
tks := tp.Tasks()
|
tks := tp.Tasks()
|
||||||
@ -48,7 +52,7 @@ func TestBasic(t *testing.T) {
|
|||||||
func TestEvaluatePolicy(t *testing.T) {
|
func TestEvaluatePolicy(t *testing.T) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
||||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{
|
tp := NewAlternatePolicy(testPolicyName, &AlternatePolicyConfiguration{
|
||||||
Duration: 1 * time.Second,
|
Duration: 1 * time.Second,
|
||||||
OffsetTime: utcOffset + 1,
|
OffsetTime: utcOffset + 1,
|
||||||
})
|
})
|
||||||
@ -78,7 +82,7 @@ func TestEvaluatePolicy(t *testing.T) {
|
|||||||
func TestDisablePolicy(t *testing.T) {
|
func TestDisablePolicy(t *testing.T) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
||||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{
|
tp := NewAlternatePolicy(testPolicyName, &AlternatePolicyConfiguration{
|
||||||
Duration: 1 * time.Second,
|
Duration: 1 * time.Second,
|
||||||
OffsetTime: utcOffset + 1,
|
OffsetTime: utcOffset + 1,
|
||||||
})
|
})
|
||||||
@ -118,3 +122,28 @@ func TestDisablePolicy(t *testing.T) {
|
|||||||
t.Fatalf("Policy is still running after calling Disable() %d=%d", atomic.LoadInt32(&copiedCounter), atomic.LoadInt32(&counter))
|
t.Fatalf("Policy is still running after calling Disable() %d=%d", atomic.LoadInt32(&copiedCounter), atomic.LoadInt32(&counter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPolicyEqual(t *testing.T) {
|
||||||
|
tp1 := NewAlternatePolicy(testPolicyName, &AlternatePolicyConfiguration{
|
||||||
|
Duration: 1 * time.Second,
|
||||||
|
OffsetTime: 8000,
|
||||||
|
})
|
||||||
|
|
||||||
|
tp2 := NewAlternatePolicy(testPolicyName+"2", &AlternatePolicyConfiguration{
|
||||||
|
Duration: 100 * time.Second,
|
||||||
|
OffsetTime: 8000,
|
||||||
|
})
|
||||||
|
|
||||||
|
if tp1.Equal(tp2) {
|
||||||
|
t.Fatal("tp1 should not equal tp2")
|
||||||
|
}
|
||||||
|
|
||||||
|
tp3 := NewAlternatePolicy(testPolicyName, &AlternatePolicyConfiguration{
|
||||||
|
Duration: 1 * time.Second,
|
||||||
|
OffsetTime: 8000,
|
||||||
|
})
|
||||||
|
|
||||||
|
if !tp1.Equal(tp3) {
|
||||||
|
t.Fatal("tp1 should equal tp3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
//
|
//
|
||||||
type Policy interface {
|
type Policy interface {
|
||||||
//Name will return the name of the policy.
|
//Name will return the name of the policy.
|
||||||
|
//If the policy supports multiple instances, please make sure the name is unique as an UUID.
|
||||||
Name() string
|
Name() string
|
||||||
|
|
||||||
//Tasks will return the attached tasks with this policy.
|
//Tasks will return the attached tasks with this policy.
|
||||||
|
22
src/common/scheduler/policy/uuid.go
Normal file
22
src/common/scheduler/policy/uuid.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
//NewUUID will generate a new UUID.
|
||||||
|
//Code copied from https://play.golang.org/p/4FkNSiUDMg
|
||||||
|
func newUUID() (string, error) {
|
||||||
|
uuid := make([]byte, 16)
|
||||||
|
n, err := io.ReadFull(rand.Reader, uuid)
|
||||||
|
if n != len(uuid) || err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid[8] = uuid[8]&^0xc0 | 0x80
|
||||||
|
uuid[6] = uuid[6]&^0xf0 | 0x40
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
|
||||||
|
}
|
31
src/common/scheduler/task/replication/replication_task.go
Normal file
31
src/common/scheduler/task/replication/replication_task.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package replication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/notifier"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/notification"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/topic"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Task is the task for triggering one replication
|
||||||
|
type Task struct {
|
||||||
|
PolicyID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewTask is constructor of creating ReplicationTask
|
||||||
|
func NewTask(policyID int64) *Task {
|
||||||
|
return &Task{
|
||||||
|
PolicyID: policyID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Name returns the name of this task
|
||||||
|
func (t *Task) Name() string {
|
||||||
|
return "replication"
|
||||||
|
}
|
||||||
|
|
||||||
|
//Run the actions here
|
||||||
|
func (t *Task) Run() error {
|
||||||
|
return notifier.Publish(topic.StartReplicationTopic, notification.StartReplicationNotification{
|
||||||
|
PolicyID: t.PolicyID,
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package replication
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTask(t *testing.T) {
|
||||||
|
tk := NewTask(1)
|
||||||
|
if tk == nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
if tk.Name() != "replication" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTask(t *testing.T) {
|
func TestScanAllTask(t *testing.T) {
|
||||||
tk := NewScanAllTask()
|
tk := NewScanAllTask()
|
||||||
if tk == nil {
|
if tk == nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
@ -23,9 +23,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/distribution/registry/auth/token"
|
"github.com/docker/distribution/registry/auth/token"
|
||||||
|
"github.com/vmware/harbor/src/common/http/modifier"
|
||||||
"github.com/vmware/harbor/src/common/models"
|
"github.com/vmware/harbor/src/common/models"
|
||||||
"github.com/vmware/harbor/src/common/utils/log"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
"github.com/vmware/harbor/src/common/utils/registry"
|
|
||||||
token_util "github.com/vmware/harbor/src/ui/service/token"
|
token_util "github.com/vmware/harbor/src/ui/service/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -254,7 +254,7 @@ func ping(client *http.Client, endpoint string) (string, string, error) {
|
|||||||
// from token server and add it to the origin request
|
// from token server and add it to the origin request
|
||||||
// If customizedTokenService is set, the token request will be sent to it instead of the server get from authorizer
|
// If customizedTokenService is set, the token request will be sent to it instead of the server get from authorizer
|
||||||
func NewStandardTokenAuthorizer(client *http.Client, credential Credential,
|
func NewStandardTokenAuthorizer(client *http.Client, credential Credential,
|
||||||
customizedTokenService ...string) registry.Modifier {
|
customizedTokenService ...string) modifier.Modifier {
|
||||||
generator := &standardTokenGenerator{
|
generator := &standardTokenGenerator{
|
||||||
credential: credential,
|
credential: credential,
|
||||||
client: client,
|
client: client,
|
||||||
@ -309,7 +309,7 @@ func (s *standardTokenGenerator) generate(scopes []*token.ResourceActions, endpo
|
|||||||
|
|
||||||
// NewRawTokenAuthorizer returns a token authorizer which calls method to create
|
// NewRawTokenAuthorizer returns a token authorizer which calls method to create
|
||||||
// token directly
|
// token directly
|
||||||
func NewRawTokenAuthorizer(username, service string) registry.Modifier {
|
func NewRawTokenAuthorizer(username, service string) modifier.Modifier {
|
||||||
generator := &rawTokenGenerator{
|
generator := &rawTokenGenerator{
|
||||||
service: service,
|
service: service,
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -17,17 +17,18 @@ package registry
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/http/modifier"
|
||||||
"github.com/vmware/harbor/src/common/utils/log"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transport holds information about base transport and modifiers
|
// Transport holds information about base transport and modifiers
|
||||||
type Transport struct {
|
type Transport struct {
|
||||||
transport http.RoundTripper
|
transport http.RoundTripper
|
||||||
modifiers []Modifier
|
modifiers []modifier.Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTransport ...
|
// NewTransport ...
|
||||||
func NewTransport(transport http.RoundTripper, modifiers ...Modifier) *Transport {
|
func NewTransport(transport http.RoundTripper, modifiers ...modifier.Modifier) *Transport {
|
||||||
return &Transport{
|
return &Transport{
|
||||||
transport: transport,
|
transport: transport,
|
||||||
modifiers: modifiers,
|
modifiers: modifiers,
|
||||||
|
45
src/common/utils/test/policy_manager.go
Normal file
45
src/common/utils/test/policy_manager.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakePolicyManager struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakePolicyManager) GetPolicies(query models.QueryParameter) ([]models.ReplicationPolicy, error) {
|
||||||
|
return []models.ReplicationPolicy{}, 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
|
||||||
|
}
|
26
src/common/utils/test/replication_controllter.go
Normal file
26
src/common/utils/test/replication_controllter.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
65
src/common/utils/test/watch_item.go
Normal file
65
src/common/utils/test/watch_item.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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/vmware/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
|
||||||
|
}
|
76
src/jobservice/client/client.go
Normal file
76
src/jobservice/client/client.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/http"
|
||||||
|
"github.com/vmware/harbor/src/common/http/modifier/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replication holds information for submiting a replication job
|
||||||
|
type Replication struct {
|
||||||
|
PolicyID int64 `json:"policy_id"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client defines the methods that a jobservice client should implement
|
||||||
|
type Client interface {
|
||||||
|
SubmitReplicationJob(*Replication) error
|
||||||
|
StopReplicationJobs(policyID int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultClient provides a default implement for the interface Client
|
||||||
|
type DefaultClient struct {
|
||||||
|
endpoint string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains configuration items needed for DefaultClient
|
||||||
|
type Config struct {
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultClient returns an instance of DefaultClient
|
||||||
|
func NewDefaultClient(endpoint string, cfg *Config) *DefaultClient {
|
||||||
|
c := &DefaultClient{
|
||||||
|
endpoint: endpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg != nil {
|
||||||
|
c.client = http.NewClient(nil, auth.NewSecretAuthorizer(cfg.Secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitReplicationJob submits a replication job to the jobservice
|
||||||
|
func (d *DefaultClient) SubmitReplicationJob(replication *Replication) error {
|
||||||
|
url := d.endpoint + "/api/jobs/replication"
|
||||||
|
return d.client.Post(url, replication)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopReplicationJobs stop replication jobs of the policy specified by the policy ID
|
||||||
|
func (d *DefaultClient) StopReplicationJobs(policyID int64) error {
|
||||||
|
url := d.endpoint + "/api/jobs/replication/actions"
|
||||||
|
return d.client.Post(url, &struct {
|
||||||
|
PolicyID int64 `json:"policy_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
}{
|
||||||
|
PolicyID: policyID,
|
||||||
|
Action: "stop",
|
||||||
|
})
|
||||||
|
}
|
86
src/jobservice/client/client_test.go
Normal file
86
src/jobservice/client/client_test.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var url string
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
requestMapping := []*test.RequestHandlerMapping{
|
||||||
|
&test.RequestHandlerMapping{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Pattern: "/api/jobs/replication/actions",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
action := &struct {
|
||||||
|
PolicyID int64 `json:"policy_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
}{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(action); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.PolicyID != 1 {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&test.RequestHandlerMapping{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Pattern: "/api/jobs/replication",
|
||||||
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
replication := &Replication{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(replication); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
server := test.NewServer(requestMapping...)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
url = server.URL
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubmitReplicationJob(t *testing.T) {
|
||||||
|
client := NewDefaultClient(url, &Config{})
|
||||||
|
err := client.SubmitReplicationJob(&Replication{})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStopReplicationJobs(t *testing.T) {
|
||||||
|
client := NewDefaultClient(url, &Config{})
|
||||||
|
|
||||||
|
// 404
|
||||||
|
err := client.StopReplicationJobs(2)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
// 200
|
||||||
|
err = client.StopReplicationJobs(1)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
@ -20,7 +20,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/vmware/harbor/src/adminserver/client"
|
"github.com/vmware/harbor/src/adminserver/client"
|
||||||
"github.com/vmware/harbor/src/adminserver/client/auth"
|
|
||||||
"github.com/vmware/harbor/src/common"
|
"github.com/vmware/harbor/src/common"
|
||||||
comcfg "github.com/vmware/harbor/src/common/config"
|
comcfg "github.com/vmware/harbor/src/common/config"
|
||||||
"github.com/vmware/harbor/src/common/models"
|
"github.com/vmware/harbor/src/common/models"
|
||||||
@ -50,8 +49,10 @@ func Init() error {
|
|||||||
adminServerURL = "http://adminserver"
|
adminServerURL = "http://adminserver"
|
||||||
}
|
}
|
||||||
log.Infof("initializing client for adminserver %s ...", adminServerURL)
|
log.Infof("initializing client for adminserver %s ...", adminServerURL)
|
||||||
authorizer := auth.NewSecretAuthorizer(secretCookieName, UISecret())
|
cfg := &client.Config{
|
||||||
AdminserverClient = client.NewClient(adminServerURL, authorizer)
|
Secret: UISecret(),
|
||||||
|
}
|
||||||
|
AdminserverClient = client.NewClient(adminServerURL, cfg)
|
||||||
if err := AdminserverClient.Ping(); err != nil {
|
if err := AdminserverClient.Ping(); err != nil {
|
||||||
return fmt.Errorf("failed to ping adminserver: %v", err)
|
return fmt.Errorf("failed to ping adminserver: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,6 @@ func TestRepJob(t *testing.T) {
|
|||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
j, err := dao.GetRepJob(repJobID)
|
j, err := dao.GetRepJob(repJobID)
|
||||||
assert.Equal(models.JobRetrying, j.Status)
|
assert.Equal(models.JobRetrying, j.Status)
|
||||||
assert.Equal(1, rj.parm.Enabled)
|
|
||||||
assert.False(rj.parm.Insecure)
|
assert.False(rj.parm.Insecure)
|
||||||
rj2 := NewRepJob(99999)
|
rj2 := NewRepJob(99999)
|
||||||
err = rj2.Init()
|
err = rj2.Init()
|
||||||
@ -163,7 +162,6 @@ func prepareRepJobData() error {
|
|||||||
}
|
}
|
||||||
policy := models.RepPolicy{
|
policy := models.RepPolicy{
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
Enabled: 1,
|
|
||||||
TargetID: targetID,
|
TargetID: targetID,
|
||||||
Description: "whatever",
|
Description: "whatever",
|
||||||
Name: "mypolicy",
|
Name: "mypolicy",
|
||||||
|
@ -62,7 +62,6 @@ type RepJobParm struct {
|
|||||||
TargetPassword string
|
TargetPassword string
|
||||||
Repository string
|
Repository string
|
||||||
Tags []string
|
Tags []string
|
||||||
Enabled int
|
|
||||||
Operation string
|
Operation string
|
||||||
Insecure bool
|
Insecure bool
|
||||||
}
|
}
|
||||||
@ -124,13 +123,8 @@ func (rj *RepJob) Init() error {
|
|||||||
LocalRegURL: regURL,
|
LocalRegURL: regURL,
|
||||||
Repository: job.Repository,
|
Repository: job.Repository,
|
||||||
Tags: job.TagList,
|
Tags: job.TagList,
|
||||||
Enabled: policy.Enabled,
|
|
||||||
Operation: job.Operation,
|
Operation: job.Operation,
|
||||||
}
|
}
|
||||||
if policy.Enabled == 0 {
|
|
||||||
//worker will cancel this job
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
target, err := dao.GetRepTarget(policy.TargetID)
|
target, err := dao.GetRepTarget(policy.TargetID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to get target, error: %v", err)
|
return fmt.Errorf("Failed to get target, error: %v", err)
|
||||||
|
@ -208,16 +208,6 @@ func (sm *SM) Reset(j Job) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SM) kickOff() error {
|
func (sm *SM) kickOff() error {
|
||||||
if repJob, ok := sm.CurrentJob.(*RepJob); ok {
|
|
||||||
if repJob.parm.Enabled == 0 {
|
|
||||||
log.Debugf("The policy of job:%v is disabled, will cancel the job", repJob)
|
|
||||||
_, err := sm.EnterState(models.JobCanceled)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("For job: %v, failed to update state to 'canceled', error: %v", repJob, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Debugf("In kickOff: will start job: %v", sm.CurrentJob)
|
log.Debugf("In kickOff: will start job: %v", sm.CurrentJob)
|
||||||
sm.Start(models.JobRunning)
|
sm.Start(models.JobRunning)
|
||||||
return nil
|
return nil
|
||||||
|
25
src/replication/consts.go
Normal file
25
src/replication/consts.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package replication
|
||||||
|
|
||||||
|
const (
|
||||||
|
//FilterItemKindProject : Kind of filter item is 'project'
|
||||||
|
FilterItemKindProject = "project"
|
||||||
|
//FilterItemKindRepository : Kind of filter item is 'repository'
|
||||||
|
FilterItemKindRepository = "repository"
|
||||||
|
//FilterItemKindTag : Kind of filter item is 'tag'
|
||||||
|
FilterItemKindTag = "tag"
|
||||||
|
|
||||||
|
//AdaptorKindHarbor : Kind of adaptor of Harbor
|
||||||
|
AdaptorKindHarbor = "Harbor"
|
||||||
|
|
||||||
|
//TriggerKindImmediate : Kind of trigger is 'Immediate'
|
||||||
|
TriggerKindImmediate = "immediate"
|
||||||
|
//TriggerKindSchedule : Kind of trigger is 'Schedule'
|
||||||
|
TriggerKindSchedule = "schedule"
|
||||||
|
//TriggerKindManual : Kind of trigger is 'Manual'
|
||||||
|
TriggerKindManual = "manual"
|
||||||
|
|
||||||
|
//TriggerScheduleDaily : type of scheduling is 'daily'
|
||||||
|
TriggerScheduleDaily = "daily"
|
||||||
|
//TriggerScheduleWeekly : type of scheduling is 'weekly'
|
||||||
|
TriggerScheduleWeekly = "weekly"
|
||||||
|
)
|
319
src/replication/core/controller.go
Normal file
319
src/replication/core/controller.go
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
common_models "github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/jobservice/client"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/policy"
|
||||||
|
"github.com/vmware/harbor/src/replication/replicator"
|
||||||
|
"github.com/vmware/harbor/src/replication/source"
|
||||||
|
"github.com/vmware/harbor/src/replication/target"
|
||||||
|
"github.com/vmware/harbor/src/replication/trigger"
|
||||||
|
"github.com/vmware/harbor/src/ui/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller defines the methods that a replicatoin controllter should implement
|
||||||
|
type Controller interface {
|
||||||
|
policy.Manager
|
||||||
|
Init() error
|
||||||
|
Replicate(policyID int64, metadata ...map[string]interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
//DefaultController is core module to cordinate and control the overall workflow of the
|
||||||
|
//replication modules.
|
||||||
|
type DefaultController struct {
|
||||||
|
//Indicate whether the controller has been initialized or not
|
||||||
|
initialized bool
|
||||||
|
|
||||||
|
//Manage the policies
|
||||||
|
policyManager policy.Manager
|
||||||
|
|
||||||
|
//Manage the targets
|
||||||
|
targetManager target.Manager
|
||||||
|
|
||||||
|
//Handle the things related with source
|
||||||
|
sourcer *source.Sourcer
|
||||||
|
|
||||||
|
//Manage the triggers of policies
|
||||||
|
triggerManager *trigger.Manager
|
||||||
|
|
||||||
|
//Handle the replication work
|
||||||
|
replicator replicator.Replicator
|
||||||
|
}
|
||||||
|
|
||||||
|
//Keep controller as singleton instance
|
||||||
|
var (
|
||||||
|
GlobalController Controller
|
||||||
|
)
|
||||||
|
|
||||||
|
//ControllerConfig includes related configurations required by the controller
|
||||||
|
type ControllerConfig struct {
|
||||||
|
//The capacity of the cache storing enabled triggers
|
||||||
|
CacheCapacity int
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewDefaultController is the constructor of DefaultController.
|
||||||
|
func NewDefaultController(cfg ControllerConfig) *DefaultController {
|
||||||
|
//Controller refer the default instances
|
||||||
|
ctl := &DefaultController{
|
||||||
|
policyManager: policy.NewDefaultManager(),
|
||||||
|
targetManager: target.NewDefaultManager(),
|
||||||
|
sourcer: source.NewSourcer(),
|
||||||
|
triggerManager: trigger.NewManager(cfg.CacheCapacity),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl.replicator = replicator.NewDefaultReplicator(config.GlobalJobserviceClient)
|
||||||
|
|
||||||
|
return ctl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init creates the GlobalController and inits it
|
||||||
|
func Init() error {
|
||||||
|
GlobalController = NewDefaultController(ControllerConfig{}) //Use default data
|
||||||
|
return GlobalController.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Init will initialize the controller and the sub components
|
||||||
|
func (ctl *DefaultController) Init() error {
|
||||||
|
if ctl.initialized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Build query parameters
|
||||||
|
query := models.QueryParameter{
|
||||||
|
TriggerType: replication.TriggerKindSchedule,
|
||||||
|
}
|
||||||
|
|
||||||
|
policies, err := ctl.policyManager.GetPolicies(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policies != nil && len(policies) > 0 {
|
||||||
|
for _, policy := range policies {
|
||||||
|
if err := ctl.triggerManager.SetupTrigger(&policy); err != nil {
|
||||||
|
log.Errorf("failed to setup trigger for policy %v: %v", policy, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Initialize sourcer
|
||||||
|
ctl.sourcer.Init()
|
||||||
|
|
||||||
|
ctl.initialized = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//CreatePolicy is used to create a new policy and enable it if necessary
|
||||||
|
func (ctl *DefaultController) CreatePolicy(newPolicy models.ReplicationPolicy) (int64, error) {
|
||||||
|
id, err := ctl.policyManager.CreatePolicy(newPolicy)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy.ID = id
|
||||||
|
if err = ctl.triggerManager.SetupTrigger(&newPolicy); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//UpdatePolicy will update the policy with new content.
|
||||||
|
//Parameter updatedPolicy must have the ID of the updated policy.
|
||||||
|
func (ctl *DefaultController) UpdatePolicy(updatedPolicy models.ReplicationPolicy) error {
|
||||||
|
id := updatedPolicy.ID
|
||||||
|
originPolicy, err := ctl.policyManager.GetPolicy(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if originPolicy.ID == 0 {
|
||||||
|
return fmt.Errorf("policy %d not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
reset := false
|
||||||
|
if updatedPolicy.Trigger.Kind != originPolicy.Trigger.Kind {
|
||||||
|
reset = true
|
||||||
|
} else {
|
||||||
|
switch updatedPolicy.Trigger.Kind {
|
||||||
|
case replication.TriggerKindSchedule:
|
||||||
|
if !originPolicy.Trigger.ScheduleParam.Equal(updatedPolicy.Trigger.ScheduleParam) {
|
||||||
|
reset = true
|
||||||
|
}
|
||||||
|
case replication.TriggerKindImmediate:
|
||||||
|
// Always reset immediate trigger as it is relevent with namespaces
|
||||||
|
reset = true
|
||||||
|
default:
|
||||||
|
// manual trigger, no need to reset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ctl.policyManager.UpdatePolicy(updatedPolicy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if reset {
|
||||||
|
if err = ctl.triggerManager.UnsetTrigger(&originPolicy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctl.triggerManager.SetupTrigger(&updatedPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//RemovePolicy will remove the specified policy and clean the related settings
|
||||||
|
func (ctl *DefaultController) RemovePolicy(policyID int64) error {
|
||||||
|
// TODO check pre-conditions
|
||||||
|
|
||||||
|
policy, err := ctl.policyManager.GetPolicy(policyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.ID == 0 {
|
||||||
|
return fmt.Errorf("policy %d not found", policyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ctl.triggerManager.UnsetTrigger(&policy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctl.policyManager.RemovePolicy(policyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetPolicy is delegation of GetPolicy of Policy.Manager
|
||||||
|
func (ctl *DefaultController) GetPolicy(policyID int64) (models.ReplicationPolicy, error) {
|
||||||
|
return ctl.policyManager.GetPolicy(policyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetPolicies is delegation of GetPoliciemodels.ReplicationPolicy{}s of Policy.Manager
|
||||||
|
func (ctl *DefaultController) GetPolicies(query models.QueryParameter) ([]models.ReplicationPolicy, error) {
|
||||||
|
return ctl.policyManager.GetPolicies(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Replicate starts one replication defined in the specified policy;
|
||||||
|
//Can be launched by the API layer and related triggers.
|
||||||
|
func (ctl *DefaultController) Replicate(policyID int64, metadata ...map[string]interface{}) error {
|
||||||
|
policy, err := ctl.GetPolicy(policyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policy.ID == 0 {
|
||||||
|
return fmt.Errorf("policy %d not found", policyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare candidates for replication
|
||||||
|
candidates := getCandidates(&policy, ctl.sourcer, metadata...)
|
||||||
|
|
||||||
|
/*
|
||||||
|
targets := []*common_models.RepTarget{}
|
||||||
|
for _, targetID := range policy.TargetIDs {
|
||||||
|
target, err := ctl.targetManager.GetTarget(targetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targets = append(targets, target)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// submit the replication
|
||||||
|
return replicate(ctl.replicator, policyID, candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCandidates(policy *models.ReplicationPolicy, sourcer *source.Sourcer,
|
||||||
|
metadata ...map[string]interface{}) []models.FilterItem {
|
||||||
|
candidates := []models.FilterItem{}
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
meta := metadata[0]["candidates"]
|
||||||
|
if meta != nil {
|
||||||
|
cands, ok := meta.([]models.FilterItem)
|
||||||
|
if ok {
|
||||||
|
candidates = append(candidates, cands...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
for _, namespace := range policy.Namespaces {
|
||||||
|
candidates = append(candidates, models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
Value: namespace,
|
||||||
|
Operation: common_models.RepOpTransfer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain := buildFilterChain(policy, sourcer)
|
||||||
|
|
||||||
|
return filterChain.DoFilter(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFilterChain(policy *models.ReplicationPolicy, sourcer *source.Sourcer) source.FilterChain {
|
||||||
|
filters := []source.Filter{}
|
||||||
|
|
||||||
|
patterns := map[string]string{}
|
||||||
|
for _, f := range policy.Filters {
|
||||||
|
patterns[f.Kind] = f.Pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
registry := sourcer.GetAdaptor(replication.AdaptorKindHarbor)
|
||||||
|
// only support repository and tag filter for now
|
||||||
|
filters = append(filters,
|
||||||
|
source.NewRepositoryFilter(patterns[replication.FilterItemKindRepository], registry))
|
||||||
|
filters = append(filters,
|
||||||
|
source.NewTagFilter(patterns[replication.FilterItemKindTag], registry))
|
||||||
|
|
||||||
|
return source.NewDefaultFilterChain(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replicate(replicator replicator.Replicator, policyID int64, candidates []models.FilterItem) error {
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
log.Debugf("replicaton candidates are null, no further action needed")
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories := map[string][]string{}
|
||||||
|
// TODO the operation of all candidates are same for now. Update it after supporting
|
||||||
|
// replicate deletion
|
||||||
|
operation := ""
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
strs := strings.SplitN(candidate.Value, ":", 2)
|
||||||
|
repositories[strs[0]] = append(repositories[strs[0]], strs[1])
|
||||||
|
operation = candidate.Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
for repository, tags := range repositories {
|
||||||
|
replication := &client.Replication{
|
||||||
|
PolicyID: policyID,
|
||||||
|
Repository: repository,
|
||||||
|
Operation: operation,
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
log.Debugf("submiting replication job to jobservice: %v", replication)
|
||||||
|
if err := replicator.Replicate(replication); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
142
src/replication/core/controller_test.go
Normal file
142
src/replication/core/controller_test.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/test"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
GlobalController = NewDefaultController(ControllerConfig{})
|
||||||
|
// set the policy manager used by GlobalController with a fake policy manager
|
||||||
|
controller := GlobalController.(*DefaultController)
|
||||||
|
controller.policyManager = &test.FakePolicyManager{}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDefaultController(t *testing.T) {
|
||||||
|
controller := NewDefaultController(ControllerConfig{})
|
||||||
|
assert.NotNil(t, controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
assert.Nil(t, GlobalController.Init())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreatePolicy(t *testing.T) {
|
||||||
|
_, err := GlobalController.CreatePolicy(models.ReplicationPolicy{
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindManual,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatePolicy(t *testing.T) {
|
||||||
|
assert.Nil(t, GlobalController.UpdatePolicy(models.ReplicationPolicy{
|
||||||
|
ID: 2,
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindManual,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemovePolicy(t *testing.T) {
|
||||||
|
assert.Nil(t, GlobalController.RemovePolicy(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPolicy(t *testing.T) {
|
||||||
|
_, err := GlobalController.GetPolicy(1)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPolicies(t *testing.T) {
|
||||||
|
_, err := GlobalController.GetPolicies(models.QueryParameter{})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplicate(t *testing.T) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCandidates(t *testing.T) {
|
||||||
|
policy := &models.ReplicationPolicy{
|
||||||
|
ID: 1,
|
||||||
|
Filters: []models.Filter{
|
||||||
|
models.Filter{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Pattern: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindImmediate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcer := source.NewSourcer()
|
||||||
|
|
||||||
|
candidates := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world:release-1.0",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world:latest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
metadata := map[string]interface{}{
|
||||||
|
"candidates": candidates,
|
||||||
|
}
|
||||||
|
result := getCandidates(policy, sourcer, metadata)
|
||||||
|
assert.Equal(t, 2, len(result))
|
||||||
|
|
||||||
|
policy.Filters = []models.Filter{
|
||||||
|
models.Filter{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Pattern: "release-*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result = getCandidates(policy, sourcer, metadata)
|
||||||
|
assert.Equal(t, 1, len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFilterChain(t *testing.T) {
|
||||||
|
policy := &models.ReplicationPolicy{
|
||||||
|
ID: 1,
|
||||||
|
Filters: []models.Filter{
|
||||||
|
models.Filter{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Pattern: "*",
|
||||||
|
},
|
||||||
|
models.Filter{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Pattern: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcer := source.NewSourcer()
|
||||||
|
|
||||||
|
chain := buildFilterChain(policy, sourcer)
|
||||||
|
assert.Equal(t, 2, len(chain.Filters()))
|
||||||
|
}
|
39
src/replication/event/init.go
Normal file
39
src/replication/event/init.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/notifier"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/topic"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Subscribe related topics
|
||||||
|
func init() {
|
||||||
|
//Listen the related event topics
|
||||||
|
handlers := map[string]notifier.NotificationHandler{
|
||||||
|
topic.StartReplicationTopic: &StartReplicationHandler{},
|
||||||
|
topic.ReplicationEventTopicOnPush: &OnPushHandler{},
|
||||||
|
topic.ReplicationEventTopicOnDeletion: &OnDeletionHandler{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for topic, handler := range handlers {
|
||||||
|
if err := notifier.Subscribe(topic, handler); err != nil {
|
||||||
|
log.Errorf("failed to subscribe topic %s: %v", topic, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Debugf("topic %s is subscribed", topic)
|
||||||
|
}
|
||||||
|
}
|
34
src/replication/event/notification/notification.go
Normal file
34
src/replication/event/notification/notification.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 notification
|
||||||
|
|
||||||
|
//OnPushNotification contains the data required by this handler
|
||||||
|
type OnPushNotification struct {
|
||||||
|
//The name of the image that is being pushed
|
||||||
|
Image string
|
||||||
|
}
|
||||||
|
|
||||||
|
//OnDeletionNotification contains the data required by this handler
|
||||||
|
type OnDeletionNotification struct {
|
||||||
|
//The name of the image that is being deleted
|
||||||
|
Image string
|
||||||
|
}
|
||||||
|
|
||||||
|
//StartReplicationNotification contains data required by this handler
|
||||||
|
type StartReplicationNotification struct {
|
||||||
|
//ID of the policy
|
||||||
|
PolicyID int64
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
48
src/replication/event/on_deletion_handler.go
Normal file
48
src/replication/event/on_deletion_handler.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
//OnDeletionHandler implements the notification handler interface to handle image on push event.
|
||||||
|
type OnDeletionHandler struct{}
|
||||||
|
|
||||||
|
//Handle implements the same method of notification handler interface
|
||||||
|
func (oph *OnDeletionHandler) Handle(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return errors.New("OnDeletionHandler can not handle nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
vType := reflect.TypeOf(value)
|
||||||
|
if vType.Kind() != reflect.Struct || vType.String() != "notification.OnDeletionNotification" {
|
||||||
|
return fmt.Errorf("Mismatch value type of OnDeletionHandler, expect %s but got %s", "notification.OnDeletionNotification", vType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
notification := value.(notification.OnDeletionNotification)
|
||||||
|
return checkAndTriggerReplication(notification.Image, models.RepOpDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
//IsStateful implements the same method of notification handler interface
|
||||||
|
func (oph *OnDeletionHandler) IsStateful() bool {
|
||||||
|
//Statless
|
||||||
|
return false
|
||||||
|
}
|
43
src/replication/event/on_deletion_handler_test.go
Normal file
43
src/replication/event/on_deletion_handler_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/test"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleOfOnDeletionHandler(t *testing.T) {
|
||||||
|
dao.DefaultDatabaseWatchItemDAO = &test.FakeWatchItemDAO{}
|
||||||
|
|
||||||
|
handler := &OnDeletionHandler{}
|
||||||
|
|
||||||
|
assert.NotNil(t, handler.Handle(nil))
|
||||||
|
assert.NotNil(t, handler.Handle(map[string]string{}))
|
||||||
|
assert.NotNil(t, handler.Handle(struct{}{}))
|
||||||
|
|
||||||
|
assert.Nil(t, handler.Handle(notification.OnDeletionNotification{
|
||||||
|
Image: "library/hello-world:latest",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsStatefulOfOnDeletionHandler(t *testing.T) {
|
||||||
|
handler := &OnDeletionHandler{}
|
||||||
|
assert.False(t, handler.IsStateful())
|
||||||
|
}
|
91
src/replication/event/on_push_handler.go
Normal file
91
src/replication/event/on_push_handler.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
common_models "github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/common/notifier"
|
||||||
|
"github.com/vmware/harbor/src/common/utils"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/notification"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/topic"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/trigger"
|
||||||
|
)
|
||||||
|
|
||||||
|
//OnPushHandler implements the notification handler interface to handle image on push event.
|
||||||
|
type OnPushHandler struct{}
|
||||||
|
|
||||||
|
//Handle implements the same method of notification handler interface
|
||||||
|
func (oph *OnPushHandler) Handle(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return errors.New("OnPushHandler can not handle nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
vType := reflect.TypeOf(value)
|
||||||
|
if vType.Kind() != reflect.Struct || vType.String() != "notification.OnPushNotification" {
|
||||||
|
return fmt.Errorf("Mismatch value type of OnPushHandler, expect %s but got %s", "notification.OnPushNotification", vType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
notification := value.(notification.OnPushNotification)
|
||||||
|
|
||||||
|
return checkAndTriggerReplication(notification.Image, common_models.RepOpTransfer)
|
||||||
|
}
|
||||||
|
|
||||||
|
//IsStateful implements the same method of notification handler interface
|
||||||
|
func (oph *OnPushHandler) IsStateful() bool {
|
||||||
|
//Statless
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks whether replication policy is set on the resource, if is, trigger the replication
|
||||||
|
func checkAndTriggerReplication(image, operation string) error {
|
||||||
|
project, _ := utils.ParseRepository(image)
|
||||||
|
watchItems, err := trigger.DefaultWatchList.Get(project, operation)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get watch list for resource %s, operation %s: %v",
|
||||||
|
image, operation, err)
|
||||||
|
}
|
||||||
|
if len(watchItems) == 0 {
|
||||||
|
log.Debugf("no replication should be triggered for resource %s, operation %s, skip", image, operation)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, watchItem := range watchItems {
|
||||||
|
item := models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: image,
|
||||||
|
Operation: operation,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := notifier.Publish(topic.StartReplicationTopic, notification.StartReplicationNotification{
|
||||||
|
PolicyID: watchItem.PolicyID,
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"candidates": []models.FilterItem{item},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to publish replication topic for resource %s, operation %s, policy %d: %v",
|
||||||
|
image, operation, watchItem.PolicyID, err)
|
||||||
|
}
|
||||||
|
log.Infof("replication topic for resource %s, operation %s, policy %d triggered",
|
||||||
|
image, operation, watchItem.PolicyID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
43
src/replication/event/on_push_handler_test.go
Normal file
43
src/replication/event/on_push_handler_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/test"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleOfOnPushHandler(t *testing.T) {
|
||||||
|
dao.DefaultDatabaseWatchItemDAO = &test.FakeWatchItemDAO{}
|
||||||
|
|
||||||
|
handler := &OnPushHandler{}
|
||||||
|
|
||||||
|
assert.NotNil(t, handler.Handle(nil))
|
||||||
|
assert.NotNil(t, handler.Handle(map[string]string{}))
|
||||||
|
assert.NotNil(t, handler.Handle(struct{}{}))
|
||||||
|
|
||||||
|
assert.Nil(t, handler.Handle(notification.OnPushNotification{
|
||||||
|
Image: "library/hello-world:latest",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsStatefulOfOnPushHandler(t *testing.T) {
|
||||||
|
handler := &OnPushHandler{}
|
||||||
|
assert.False(t, handler.IsStateful())
|
||||||
|
}
|
53
src/replication/event/start_replication_handler.go
Normal file
53
src/replication/event/start_replication_handler.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/replication/core"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
//StartReplicationHandler implements the notification handler interface to handle start replication requests.
|
||||||
|
type StartReplicationHandler struct{}
|
||||||
|
|
||||||
|
//Handle implements the same method of notification handler interface
|
||||||
|
func (srh *StartReplicationHandler) Handle(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return errors.New("StartReplicationHandler can not handle nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
vType := reflect.TypeOf(value)
|
||||||
|
if vType.Kind() != reflect.Struct || vType.String() != "notification.StartReplicationNotification" {
|
||||||
|
return fmt.Errorf("Mismatch value type of StartReplicationHandler, expect %s but got %s", "notification.StartReplicationNotification", vType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
notification := value.(notification.StartReplicationNotification)
|
||||||
|
if notification.PolicyID <= 0 {
|
||||||
|
return errors.New("Invalid policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Start replication
|
||||||
|
return core.GlobalController.Replicate(notification.PolicyID, notification.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
//IsStateful implements the same method of notification handler interface
|
||||||
|
func (srh *StartReplicationHandler) IsStateful() bool {
|
||||||
|
//Stateless
|
||||||
|
return false
|
||||||
|
}
|
45
src/replication/event/start_replication_handler_test.go
Normal file
45
src/replication/event/start_replication_handler_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/test"
|
||||||
|
"github.com/vmware/harbor/src/replication/core"
|
||||||
|
"github.com/vmware/harbor/src/replication/event/notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandle(t *testing.T) {
|
||||||
|
core.GlobalController = &test.FakeReplicatoinController{}
|
||||||
|
|
||||||
|
handler := &StartReplicationHandler{}
|
||||||
|
|
||||||
|
assert.NotNil(t, handler.Handle(nil))
|
||||||
|
assert.NotNil(t, handler.Handle(map[string]string{}))
|
||||||
|
assert.NotNil(t, handler.Handle(struct{}{}))
|
||||||
|
assert.NotNil(t, handler.Handle(notification.StartReplicationNotification{
|
||||||
|
PolicyID: -1,
|
||||||
|
}))
|
||||||
|
assert.Nil(t, handler.Handle(notification.StartReplicationNotification{
|
||||||
|
PolicyID: 1,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsStateful(t *testing.T) {
|
||||||
|
handler := &StartReplicationHandler{}
|
||||||
|
assert.False(t, handler.IsStateful())
|
||||||
|
}
|
12
src/replication/event/topic/topics.go
Normal file
12
src/replication/event/topic/topics.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package topic
|
||||||
|
|
||||||
|
const (
|
||||||
|
//ReplicationEventTopicOnPush : OnPush event
|
||||||
|
ReplicationEventTopicOnPush = "OnPush"
|
||||||
|
|
||||||
|
//ReplicationEventTopicOnDeletion : OnDeletion event
|
||||||
|
ReplicationEventTopicOnDeletion = "OnDeletion"
|
||||||
|
|
||||||
|
//StartReplicationTopic : Start application request
|
||||||
|
StartReplicationTopic = "StartReplication"
|
||||||
|
)
|
41
src/replication/models/filter.go
Normal file
41
src/replication/models/filter.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego/validation"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter is the data model represents the filter defined by user.
|
||||||
|
type Filter struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Pattern string `json:"pattern"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid ...
|
||||||
|
func (f *Filter) Valid(v *validation.Validation) {
|
||||||
|
if !(f.Kind == replication.FilterItemKindProject ||
|
||||||
|
f.Kind == replication.FilterItemKindRepository ||
|
||||||
|
f.Kind == replication.FilterItemKindTag) {
|
||||||
|
v.SetError("kind", fmt.Sprintf("invalid filter kind: %s", f.Kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.Pattern) == 0 {
|
||||||
|
v.SetError("pattern", "filter pattern can not be empty")
|
||||||
|
}
|
||||||
|
}
|
7
src/replication/models/filter_config.go
Normal file
7
src/replication/models/filter_config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
//FilterConfig is data model to provide configurations to the filters.
|
||||||
|
type FilterConfig struct {
|
||||||
|
//The pattern for fuzzy matching
|
||||||
|
Pattern string
|
||||||
|
}
|
35
src/replication/models/filter_item.go
Normal file
35
src/replication/models/filter_item.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
//FilterItem is the general data model represents the filtering resources which are used as input and output for the filters.
|
||||||
|
type FilterItem struct {
|
||||||
|
|
||||||
|
//The kind of the filtering resources. Support 'project','repository' and 'tag' etc.
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
|
||||||
|
//The key value of resource which can be used to filter out the resource matched with specified pattern.
|
||||||
|
//E.g:
|
||||||
|
//kind == 'project', value will be project name;
|
||||||
|
//kind == 'repository', value will be repository name
|
||||||
|
//kind == 'tag', value will be tag name.
|
||||||
|
Value string `json:"value"`
|
||||||
|
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
|
||||||
|
//Extension placeholder.
|
||||||
|
//To append more additional information if required by the filter.
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
}
|
45
src/replication/models/filter_test.go
Normal file
45
src/replication/models/filter_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego/validation"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValid(t *testing.T) {
|
||||||
|
cases := map[*Filter]bool{
|
||||||
|
&Filter{}: true,
|
||||||
|
&Filter{
|
||||||
|
Kind: "invalid_kind",
|
||||||
|
}: true,
|
||||||
|
&Filter{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
}: true,
|
||||||
|
&Filter{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Pattern: "*",
|
||||||
|
}: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for filter, hasError := range cases {
|
||||||
|
v := &validation.Validation{}
|
||||||
|
filter.Valid(v)
|
||||||
|
assert.Equal(t, hasError, v.HasErrors())
|
||||||
|
}
|
||||||
|
}
|
38
src/replication/models/policy.go
Normal file
38
src/replication/models/policy.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//ReplicationPolicy defines the structure of a replication policy.
|
||||||
|
type ReplicationPolicy struct {
|
||||||
|
ID int64 //UUID of the policy
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Filters []Filter
|
||||||
|
ReplicateDeletion bool
|
||||||
|
Trigger *Trigger //The trigger of the replication
|
||||||
|
ProjectIDs []int64 //Projects attached to this policy
|
||||||
|
TargetIDs []int64
|
||||||
|
Namespaces []string // The namespaces are used to set immediate trigger
|
||||||
|
CreationTime time.Time
|
||||||
|
UpdateTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
//QueryParameter defines the parameters used to do query selection.
|
||||||
|
type QueryParameter struct {
|
||||||
|
//Query by page, couple with pageSize
|
||||||
|
Page int64
|
||||||
|
|
||||||
|
//Size of each page, couple with page
|
||||||
|
PageSize int64
|
||||||
|
|
||||||
|
//Query by the type of trigger
|
||||||
|
TriggerType string
|
||||||
|
|
||||||
|
//Query by project ID
|
||||||
|
ProjectID int64
|
||||||
|
|
||||||
|
//Query by name
|
||||||
|
Name string
|
||||||
|
}
|
34
src/replication/models/registry_models.go
Normal file
34
src/replication/models/registry_models.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
//Namespace is the resource group/scope like project in Harbor and organization in docker hub.
|
||||||
|
type Namespace struct {
|
||||||
|
//Name of the namespace
|
||||||
|
Name string
|
||||||
|
|
||||||
|
//Extensions to provide flexibility
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Repository is to keep the info of image repository.
|
||||||
|
type Repository struct {
|
||||||
|
//Name of the repository
|
||||||
|
Name string
|
||||||
|
|
||||||
|
//Project reference of this repository belongs to
|
||||||
|
Namespace Namespace
|
||||||
|
|
||||||
|
//Extensions to provide flexibility
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Tag keeps the info of image with specified version
|
||||||
|
type Tag struct {
|
||||||
|
//Name of the tag
|
||||||
|
Name string
|
||||||
|
|
||||||
|
//The repository reference of this tag belongs to
|
||||||
|
Repository Repository
|
||||||
|
|
||||||
|
//Extensions to provide flexibility
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
79
src/replication/models/trigger.go
Normal file
79
src/replication/models/trigger.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego/validation"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Trigger is replication launching approach definition
|
||||||
|
type Trigger struct {
|
||||||
|
Kind string `json:"kind"` // the type of the trigger
|
||||||
|
ScheduleParam *ScheduleParam `json:"schedule_param"` // optional, only used when kind is 'schedule'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid ...
|
||||||
|
func (t *Trigger) Valid(v *validation.Validation) {
|
||||||
|
if !(t.Kind == replication.TriggerKindImmediate ||
|
||||||
|
t.Kind == replication.TriggerKindManual ||
|
||||||
|
t.Kind == replication.TriggerKindSchedule) {
|
||||||
|
v.SetError("kind", fmt.Sprintf("invalid trigger kind: %s", t.Kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind == replication.TriggerKindSchedule {
|
||||||
|
if t.ScheduleParam == nil {
|
||||||
|
v.SetError("schedule_param", "empty schedule_param")
|
||||||
|
} else {
|
||||||
|
t.ScheduleParam.Valid(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduleParam defines the parameters used by schedule trigger
|
||||||
|
type ScheduleParam struct {
|
||||||
|
Type string `json:"type"` //daily or weekly
|
||||||
|
Weekday int8 `json:"weekday"` //Optional, only used when type is 'weekly'
|
||||||
|
Offtime int64 `json:"offtime"` //The time offset with the UTC 00:00 in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid ...
|
||||||
|
func (s *ScheduleParam) Valid(v *validation.Validation) {
|
||||||
|
if !(s.Type == replication.TriggerScheduleDaily ||
|
||||||
|
s.Type == replication.TriggerScheduleWeekly) {
|
||||||
|
v.SetError("type", fmt.Sprintf("invalid schedule trigger parameter type: %s", s.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Type == replication.TriggerScheduleWeekly {
|
||||||
|
if s.Weekday < 1 || s.Weekday > 7 {
|
||||||
|
v.SetError("weekday", fmt.Sprintf("invalid schedule trigger parameter weekday: %d", s.Weekday))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Offtime < 0 || s.Offtime > 3600*24 {
|
||||||
|
v.SetError("offtime", fmt.Sprintf("invalid schedule trigger parameter offtime: %d", s.Offtime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal ...
|
||||||
|
func (s *ScheduleParam) Equal(param *ScheduleParam) bool {
|
||||||
|
if param == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Type == param.Type && s.Weekday == param.Weekday && s.Offtime == param.Offtime
|
||||||
|
}
|
77
src/replication/models/trigger_test.go
Normal file
77
src/replication/models/trigger_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego/validation"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidOfTrigger(t *testing.T) {
|
||||||
|
cases := map[*Trigger]bool{
|
||||||
|
&Trigger{}: true,
|
||||||
|
&Trigger{
|
||||||
|
Kind: "invalid_kind",
|
||||||
|
}: true,
|
||||||
|
&Trigger{
|
||||||
|
Kind: replication.TriggerKindImmediate,
|
||||||
|
}: false,
|
||||||
|
&Trigger{
|
||||||
|
Kind: replication.TriggerKindSchedule,
|
||||||
|
}: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for filter, hasError := range cases {
|
||||||
|
v := &validation.Validation{}
|
||||||
|
filter.Valid(v)
|
||||||
|
assert.Equal(t, hasError, v.HasErrors())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidOfScheduleParam(t *testing.T) {
|
||||||
|
cases := map[*ScheduleParam]bool{
|
||||||
|
&ScheduleParam{}: true,
|
||||||
|
&ScheduleParam{
|
||||||
|
Type: "invalid_type",
|
||||||
|
}: true,
|
||||||
|
&ScheduleParam{
|
||||||
|
Type: replication.TriggerScheduleDaily,
|
||||||
|
Offtime: 3600*24 + 1,
|
||||||
|
}: true,
|
||||||
|
&ScheduleParam{
|
||||||
|
Type: replication.TriggerScheduleDaily,
|
||||||
|
Offtime: 3600 * 2,
|
||||||
|
}: false,
|
||||||
|
&ScheduleParam{
|
||||||
|
Type: replication.TriggerScheduleWeekly,
|
||||||
|
Weekday: 0,
|
||||||
|
Offtime: 3600 * 2,
|
||||||
|
}: true,
|
||||||
|
&ScheduleParam{
|
||||||
|
Type: replication.TriggerScheduleWeekly,
|
||||||
|
Weekday: 7,
|
||||||
|
Offtime: 3600 * 2,
|
||||||
|
}: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for param, hasError := range cases {
|
||||||
|
v := &validation.Validation{}
|
||||||
|
param.Valid(v)
|
||||||
|
assert.Equal(t, hasError, v.HasErrors())
|
||||||
|
}
|
||||||
|
}
|
188
src/replication/policy/manager.go
Normal file
188
src/replication/policy/manager.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
persist_models "github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/ui/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager defines the method a policy manger should implement
|
||||||
|
type Manager interface {
|
||||||
|
GetPolicies(models.QueryParameter) ([]models.ReplicationPolicy, error)
|
||||||
|
GetPolicy(int64) (models.ReplicationPolicy, error)
|
||||||
|
CreatePolicy(models.ReplicationPolicy) (int64, error)
|
||||||
|
UpdatePolicy(models.ReplicationPolicy) error
|
||||||
|
RemovePolicy(int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
//DefaultManager provides replication policy CURD capabilities.
|
||||||
|
type DefaultManager struct{}
|
||||||
|
|
||||||
|
//NewDefaultManager is the constructor of DefaultManager.
|
||||||
|
func NewDefaultManager() *DefaultManager {
|
||||||
|
return &DefaultManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetPolicies returns all the policies
|
||||||
|
func (m *DefaultManager) GetPolicies(query models.QueryParameter) ([]models.ReplicationPolicy, error) {
|
||||||
|
result := []models.ReplicationPolicy{}
|
||||||
|
//TODO support more query conditions other than name and project ID
|
||||||
|
policies, err := dao.FilterRepPolicies(query.Name, query.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, policy := range policies {
|
||||||
|
ply, err := convertFromPersistModel(policy)
|
||||||
|
if err != nil {
|
||||||
|
return []models.ReplicationPolicy{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query.TriggerType) > 0 {
|
||||||
|
if ply.Trigger.Kind != query.TriggerType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, ply)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetPolicy returns the policy with the specified ID
|
||||||
|
func (m *DefaultManager) GetPolicy(policyID int64) (models.ReplicationPolicy, error) {
|
||||||
|
policy, err := dao.GetRepPolicy(policyID)
|
||||||
|
if err != nil {
|
||||||
|
return models.ReplicationPolicy{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertFromPersistModel(policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertFromPersistModel(policy *persist_models.RepPolicy) (models.ReplicationPolicy, error) {
|
||||||
|
if policy == nil {
|
||||||
|
return models.ReplicationPolicy{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ply := models.ReplicationPolicy{
|
||||||
|
ID: policy.ID,
|
||||||
|
Name: policy.Name,
|
||||||
|
Description: policy.Description,
|
||||||
|
ReplicateDeletion: policy.ReplicateDeletion,
|
||||||
|
ProjectIDs: []int64{policy.ProjectID},
|
||||||
|
TargetIDs: []int64{policy.TargetID},
|
||||||
|
CreationTime: policy.CreationTime,
|
||||||
|
UpdateTime: policy.UpdateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := config.GlobalProjectMgr.Get(policy.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return models.ReplicationPolicy{}, err
|
||||||
|
}
|
||||||
|
ply.Namespaces = []string{project.Name}
|
||||||
|
|
||||||
|
if len(policy.Filters) > 0 {
|
||||||
|
filters := []models.Filter{}
|
||||||
|
if err := json.Unmarshal([]byte(policy.Filters), &filters); err != nil {
|
||||||
|
return models.ReplicationPolicy{}, err
|
||||||
|
}
|
||||||
|
ply.Filters = filters
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(policy.Trigger) > 0 {
|
||||||
|
trigger := &models.Trigger{}
|
||||||
|
if err := json.Unmarshal([]byte(policy.Trigger), trigger); err != nil {
|
||||||
|
return models.ReplicationPolicy{}, err
|
||||||
|
}
|
||||||
|
ply.Trigger = trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
return ply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToPersistModel(policy models.ReplicationPolicy) (*persist_models.RepPolicy, error) {
|
||||||
|
ply := &persist_models.RepPolicy{
|
||||||
|
ID: policy.ID,
|
||||||
|
Name: policy.Name,
|
||||||
|
Description: policy.Description,
|
||||||
|
ReplicateDeletion: policy.ReplicateDeletion,
|
||||||
|
CreationTime: policy.CreationTime,
|
||||||
|
UpdateTime: policy.UpdateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(policy.ProjectIDs) > 0 {
|
||||||
|
ply.ProjectID = policy.ProjectIDs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(policy.TargetIDs) > 0 {
|
||||||
|
ply.TargetID = policy.TargetIDs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.Trigger != nil {
|
||||||
|
trigger, err := json.Marshal(policy.Trigger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ply.Trigger = string(trigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(policy.Filters) > 0 {
|
||||||
|
filters, err := json.Marshal(policy.Filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ply.Filters = string(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//CreatePolicy creates a new policy with the provided data;
|
||||||
|
//If creating failed, error will be returned;
|
||||||
|
//If creating succeed, ID of the new created policy will be returned.
|
||||||
|
func (m *DefaultManager) CreatePolicy(policy models.ReplicationPolicy) (int64, error) {
|
||||||
|
now := time.Now()
|
||||||
|
policy.CreationTime = now
|
||||||
|
policy.UpdateTime = now
|
||||||
|
ply, err := convertToPersistModel(policy)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return dao.AddRepPolicy(*ply)
|
||||||
|
}
|
||||||
|
|
||||||
|
//UpdatePolicy updates the policy;
|
||||||
|
//If updating failed, error will be returned.
|
||||||
|
func (m *DefaultManager) UpdatePolicy(policy models.ReplicationPolicy) error {
|
||||||
|
policy.UpdateTime = time.Now()
|
||||||
|
ply, err := convertToPersistModel(policy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return dao.UpdateRepPolicy(ply)
|
||||||
|
}
|
||||||
|
|
||||||
|
//RemovePolicy removes the specified policy;
|
||||||
|
//If removing failed, error will be returned.
|
||||||
|
func (m *DefaultManager) RemovePolicy(policyID int64) error {
|
||||||
|
return dao.DeleteRepPolicy(policyID)
|
||||||
|
}
|
60
src/replication/policy/manager_test.go
Normal file
60
src/replication/policy/manager_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertToPersistModel(t *testing.T) {
|
||||||
|
var id, projectID, targetID int64 = 1, 1, 1
|
||||||
|
name := "policy01"
|
||||||
|
replicateDeletion := true
|
||||||
|
trigger := &models.Trigger{
|
||||||
|
Kind: "trigger_kind",
|
||||||
|
}
|
||||||
|
filters := []models.Filter{
|
||||||
|
models.Filter{
|
||||||
|
Kind: "filter_kind",
|
||||||
|
Pattern: "filter_pattern",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
policy := models.ReplicationPolicy{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
ReplicateDeletion: replicateDeletion,
|
||||||
|
ProjectIDs: []int64{projectID},
|
||||||
|
TargetIDs: []int64{targetID},
|
||||||
|
Trigger: trigger,
|
||||||
|
Filters: filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
ply, err := convertToPersistModel(policy)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, id, ply.ID)
|
||||||
|
assert.Equal(t, name, ply.Name)
|
||||||
|
assert.Equal(t, replicateDeletion, ply.ReplicateDeletion)
|
||||||
|
assert.Equal(t, projectID, ply.ProjectID)
|
||||||
|
assert.Equal(t, targetID, ply.TargetID)
|
||||||
|
tg, _ := json.Marshal(trigger)
|
||||||
|
assert.Equal(t, string(tg), ply.Trigger)
|
||||||
|
ft, _ := json.Marshal(filters)
|
||||||
|
assert.Equal(t, string(ft), ply.Filters)
|
||||||
|
}
|
34
src/replication/registry/adaptor.go
Normal file
34
src/replication/registry/adaptor.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Adaptor defines the unified operations for all the supported registries such as Harbor or DockerHub.
|
||||||
|
//It's used to adapt the different interfaces provied by the different registry providers.
|
||||||
|
//Use external registry with restful api providing as example, these intrefaces may depends on the
|
||||||
|
//related restful apis like:
|
||||||
|
// /api/vx/repositories/{namespace}/{repositoryName}/tags/{name}
|
||||||
|
// /api/v0/accounts/{namespace}
|
||||||
|
type Adaptor interface {
|
||||||
|
//Return the unique kind identifier of the adaptor
|
||||||
|
Kind() string
|
||||||
|
|
||||||
|
//Get all the namespaces
|
||||||
|
GetNamespaces() []models.Namespace
|
||||||
|
|
||||||
|
//Get the namespace with the specified name
|
||||||
|
GetNamespace(name string) models.Namespace
|
||||||
|
|
||||||
|
//Get all the repositories under the specified namespace
|
||||||
|
GetRepositories(namespace string) []models.Repository
|
||||||
|
|
||||||
|
//Get the repository with the specified name under the specified namespace
|
||||||
|
GetRepository(name string, namespace string) models.Repository
|
||||||
|
|
||||||
|
//Get all the tags of the specified repository under the namespace
|
||||||
|
GetTags(repositoryName string, namespace string) []models.Tag
|
||||||
|
|
||||||
|
//Get the tag with the specified name of the repository under the namespace
|
||||||
|
GetTag(name string, repositoryName string, namespace string) models.Tag
|
||||||
|
}
|
80
src/replication/registry/harbor_adaptor.go
Normal file
80
src/replication/registry/harbor_adaptor.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO refacotor the methods of HarborAdaptor by caling Harbor's API
|
||||||
|
|
||||||
|
//HarborAdaptor is defined to adapt the Harbor registry
|
||||||
|
type HarborAdaptor struct{}
|
||||||
|
|
||||||
|
//Kind returns the unique kind identifier of the adaptor
|
||||||
|
func (ha *HarborAdaptor) Kind() string {
|
||||||
|
return replication.AdaptorKindHarbor
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetNamespaces is ued to get all the namespaces
|
||||||
|
func (ha *HarborAdaptor) GetNamespaces() []models.Namespace {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetNamespace is used to get the namespace with the specified name
|
||||||
|
func (ha *HarborAdaptor) GetNamespace(name string) models.Namespace {
|
||||||
|
return models.Namespace{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetRepositories is used to get all the repositories under the specified namespace
|
||||||
|
func (ha *HarborAdaptor) GetRepositories(namespace string) []models.Repository {
|
||||||
|
repos, err := dao.GetRepositoryByProjectName(namespace)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get repositories under namespace %s: %v", namespace, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories := []models.Repository{}
|
||||||
|
for _, repo := range repos {
|
||||||
|
repositories = append(repositories, models.Repository{
|
||||||
|
Name: repo.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return repositories
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetRepository is used to get the repository with the specified name under the specified namespace
|
||||||
|
func (ha *HarborAdaptor) GetRepository(name string, namespace string) models.Repository {
|
||||||
|
return models.Repository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetTags is used to get all the tags of the specified repository under the namespace
|
||||||
|
func (ha *HarborAdaptor) GetTags(repositoryName string, namespace string) []models.Tag {
|
||||||
|
client, err := utils.NewRepositoryClientForUI("harbor-ui", repositoryName)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create registry client: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := client.ListTag()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get tags of repository %s: %v", repositoryName, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := []models.Tag{}
|
||||||
|
for _, t := range ts {
|
||||||
|
tags = append(tags, models.Tag{
|
||||||
|
Name: t,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetTag is used to get the tag with the specified name of the repository under the namespace
|
||||||
|
func (ha *HarborAdaptor) GetTag(name string, repositoryName string, namespace string) models.Tag {
|
||||||
|
return models.Tag{}
|
||||||
|
}
|
41
src/replication/replicator/replicator.go
Normal file
41
src/replication/replicator/replicator.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 replicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/jobservice/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replicator submits the replication work to the jobservice
|
||||||
|
type Replicator interface {
|
||||||
|
Replicate(*client.Replication) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultReplicator provides a default implement for Replicator
|
||||||
|
type DefaultReplicator struct {
|
||||||
|
client client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultReplicator returns an instance of DefaultReplicator
|
||||||
|
func NewDefaultReplicator(client client.Client) *DefaultReplicator {
|
||||||
|
return &DefaultReplicator{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replicate ...
|
||||||
|
func (d *DefaultReplicator) Replicate(replication *client.Replication) error {
|
||||||
|
return d.client.SubmitReplicationJob(replication)
|
||||||
|
}
|
@ -12,39 +12,26 @@
|
|||||||
// 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 auth
|
package replicator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/jobservice/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authorizer authorizes request
|
type fakeJobserviceClient struct{}
|
||||||
type Authorizer interface {
|
|
||||||
Authorize(*http.Request) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSecretAuthorizer returns an instance of secretAuthorizer
|
func (f *fakeJobserviceClient) SubmitReplicationJob(replication *client.Replication) error {
|
||||||
func NewSecretAuthorizer(cookieName, secret string) Authorizer {
|
|
||||||
return &secretAuthorizer{
|
|
||||||
cookieName: cookieName,
|
|
||||||
secret: secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type secretAuthorizer struct {
|
|
||||||
cookieName string
|
|
||||||
secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *secretAuthorizer) Authorize(req *http.Request) error {
|
|
||||||
if req == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
req.AddCookie(&http.Cookie{
|
func (f *fakeJobserviceClient) StopReplicationJobs(policyID int64) error {
|
||||||
Name: s.cookieName,
|
|
||||||
Value: s.secret,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReplicate(t *testing.T) {
|
||||||
|
replicator := NewDefaultReplicator(&fakeJobserviceClient{})
|
||||||
|
assert.Nil(t, replicator.Replicate(&client.Replication{}))
|
||||||
|
}
|
16
src/replication/source/convertor.go
Normal file
16
src/replication/source/convertor.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Convertor is designed to covert the format of output from upstream filter to the input format
|
||||||
|
//required by the downstream filter if needed.
|
||||||
|
//Each convertor covers only one specified conversion process between the two filters.
|
||||||
|
//E.g:
|
||||||
|
//If project filter connects to repository filter, then one convertor should be defined for this connection;
|
||||||
|
//If project filter connects to tag filter, then another one should be defined. The above one can not be reused.
|
||||||
|
type Convertor interface {
|
||||||
|
//Accept the items from upstream filter as input and then covert them to the required format and returned.
|
||||||
|
Convert(itemsOfUpstream []models.FilterItem) (itemsOfDownstream []models.FilterItem)
|
||||||
|
}
|
57
src/replication/source/default_filter_chain.go
Normal file
57
src/replication/source/default_filter_chain.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultFilterChain provides a default implement for interface FilterChain
|
||||||
|
type DefaultFilterChain struct {
|
||||||
|
filters []Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultFilterChain returns an instance of DefaultFilterChain
|
||||||
|
func NewDefaultFilterChain(filters []Filter) *DefaultFilterChain {
|
||||||
|
return &DefaultFilterChain{
|
||||||
|
filters: filters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build nil implement now
|
||||||
|
func (d *DefaultFilterChain) Build(filters []Filter) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters returns the filter list
|
||||||
|
func (d *DefaultFilterChain) Filters() []Filter {
|
||||||
|
return d.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoFilter does the filter works for filterItems
|
||||||
|
func (d *DefaultFilterChain) DoFilter(filterItems []models.FilterItem) []models.FilterItem {
|
||||||
|
if len(filterItems) == 0 {
|
||||||
|
return []models.FilterItem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filter := range d.filters {
|
||||||
|
convertor := filter.GetConvertor()
|
||||||
|
if convertor != nil {
|
||||||
|
filterItems = convertor.Convert(filterItems)
|
||||||
|
}
|
||||||
|
filterItems = filter.DoFilter(filterItems)
|
||||||
|
}
|
||||||
|
return filterItems
|
||||||
|
}
|
75
src/replication/source/default_filter_chain_test.go
Normal file
75
src/replication/source/default_filter_chain_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuild(t *testing.T) {
|
||||||
|
chain := NewDefaultFilterChain(nil)
|
||||||
|
require.Nil(t, chain.Build(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilters(t *testing.T) {
|
||||||
|
filters := []Filter{NewPatternFilter("project", "*")}
|
||||||
|
chain := NewDefaultFilterChain(filters)
|
||||||
|
assert.EqualValues(t, filters, chain.Filters())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoFilter(t *testing.T) {
|
||||||
|
projectFilter := NewPatternFilter(replication.FilterItemKindProject, "library*")
|
||||||
|
repositoryFilter := NewPatternFilter(replication.FilterItemKindRepository,
|
||||||
|
"library/ubuntu*", &fakeRepositoryConvertor{})
|
||||||
|
filters := []Filter{projectFilter, repositoryFilter}
|
||||||
|
|
||||||
|
items := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
Value: "library",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
Value: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
chain := NewDefaultFilterChain(filters)
|
||||||
|
items = chain.DoFilter(items)
|
||||||
|
assert.EqualValues(t, []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: "library/ubuntu",
|
||||||
|
},
|
||||||
|
}, items)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRepositoryConvertor struct{}
|
||||||
|
|
||||||
|
func (f *fakeRepositoryConvertor) Convert(items []models.FilterItem) []models.FilterItem {
|
||||||
|
result := []models.FilterItem{}
|
||||||
|
for _, item := range items {
|
||||||
|
result = append(result, models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: item.Value + "/ubuntu",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
18
src/replication/source/filter.go
Normal file
18
src/replication/source/filter.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Filter define the operations of selecting the matched resources from the candidates
|
||||||
|
//according to the specified pattern.
|
||||||
|
type Filter interface {
|
||||||
|
//Initialize the filter
|
||||||
|
Init() error
|
||||||
|
|
||||||
|
//Return the convertor if existing or nil if never set
|
||||||
|
GetConvertor() Convertor
|
||||||
|
|
||||||
|
//Filter the items
|
||||||
|
DoFilter(filterItems []models.FilterItem) []models.FilterItem
|
||||||
|
}
|
21
src/replication/source/filter_chain.go
Normal file
21
src/replication/source/filter_chain.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
//FilterChain is the interface to define the operations of coordinating multiple filters
|
||||||
|
//to work together as a whole pipeline.
|
||||||
|
//E.g:
|
||||||
|
//(original resources)---->[project filter]---->[repository filter]---->[tag filter]---->[......]---->(filter resources)
|
||||||
|
type FilterChain interface {
|
||||||
|
//Build the filter chain with the filters provided;
|
||||||
|
//if failed, an error will be returned.
|
||||||
|
Build(filter []Filter) error
|
||||||
|
|
||||||
|
//Return all the filters in the chain.
|
||||||
|
Filters() []Filter
|
||||||
|
|
||||||
|
//Filter the items and returned the filtered items via the appended filters in the chain.
|
||||||
|
DoFilter(filterItems []models.FilterItem) []models.FilterItem
|
||||||
|
}
|
23
src/replication/source/match.go
Normal file
23
src/replication/source/match.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func match(pattern, str string) (bool, error) {
|
||||||
|
return filepath.Match(pattern, str)
|
||||||
|
}
|
43
src/replication/source/match_test.go
Normal file
43
src/replication/source/match_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatch(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
pattern string
|
||||||
|
str string
|
||||||
|
matched bool
|
||||||
|
}{
|
||||||
|
{"", "", true},
|
||||||
|
{"*", "library", true},
|
||||||
|
{"library/*", "library/mysql", true},
|
||||||
|
{"library/*", "library/mysql/5.6", false},
|
||||||
|
{"library/mysq?", "library/mysql", true},
|
||||||
|
{"library/mysq?", "library/mysqld", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
matched, err := match(c.pattern, c.str)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, c.matched, matched)
|
||||||
|
}
|
||||||
|
}
|
84
src/replication/source/pattern_filter.go
Normal file
84
src/replication/source/pattern_filter.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PatternFilter implements Filter interface for pattern filter
|
||||||
|
type PatternFilter struct {
|
||||||
|
kind string
|
||||||
|
pattern string
|
||||||
|
convertor Convertor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPatternFilter returns an instance of PatternFilter
|
||||||
|
func NewPatternFilter(kind, pattern string, convertor ...Convertor) *PatternFilter {
|
||||||
|
filer := &PatternFilter{
|
||||||
|
kind: kind,
|
||||||
|
pattern: pattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(convertor) > 0 {
|
||||||
|
filer.convertor = convertor[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return filer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the filter. nil implement for now
|
||||||
|
func (p *PatternFilter) Init() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConvertor returns the convertor
|
||||||
|
func (p *PatternFilter) GetConvertor() Convertor {
|
||||||
|
return p.convertor
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoFilter filters resources
|
||||||
|
func (p *PatternFilter) DoFilter(filterItems []models.FilterItem) []models.FilterItem {
|
||||||
|
items := []models.FilterItem{}
|
||||||
|
for _, item := range filterItems {
|
||||||
|
if item.Kind != p.kind {
|
||||||
|
log.Warningf("unexpected filter item kind, expected: %s, got: %s, skip",
|
||||||
|
p.kind, item.Kind)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matched, err := regexp.MatchString(p.pattern, item.Value)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to match pattern %s, value %s: %v, skip",
|
||||||
|
p.pattern, item.Value, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
log.Debugf("%s does not match to the %s filter %s, skip",
|
||||||
|
item.Value, p.kind, p.pattern)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("add %s to the result of %s filter %s",
|
||||||
|
item.Value, p.kind, p.pattern)
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
63
src/replication/source/pattern_filter_test.go
Normal file
63
src/replication/source/pattern_filter_test.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pfilter = NewPatternFilter(replication.FilterItemKindTag, "library/ubuntu:release-*", nil)
|
||||||
|
|
||||||
|
func TestPatternFilterInit(t *testing.T) {
|
||||||
|
assert.Nil(t, pfilter.Init())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatternFilterGetConvertor(t *testing.T) {
|
||||||
|
assert.Nil(t, pfilter.GetConvertor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatternFilterDoFilter(t *testing.T) {
|
||||||
|
items := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:release-14.04",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:release-16.04",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := pfilter.DoFilter(items)
|
||||||
|
assert.Equal(t, 2, len(result))
|
||||||
|
assert.Equal(t, replication.FilterItemKindTag, result[0].Kind)
|
||||||
|
assert.Equal(t, "library/ubuntu:release-14.04", result[0].Value)
|
||||||
|
assert.Equal(t, replication.FilterItemKindTag, result[1].Kind)
|
||||||
|
assert.Equal(t, "library/ubuntu:release-16.04", result[1].Value)
|
||||||
|
|
||||||
|
}
|
55
src/replication/source/repository_convertor.go
Normal file
55
src/replication/source/repository_convertor.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepositoryConvertor implement Convertor interface, convert projects to repositories
|
||||||
|
type RepositoryConvertor struct {
|
||||||
|
registry registry.Adaptor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRepositoryConvertor returns an instance of RepositoryConvertor
|
||||||
|
func NewRepositoryConvertor(registry registry.Adaptor) *RepositoryConvertor {
|
||||||
|
return &RepositoryConvertor{
|
||||||
|
registry: registry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert projects to repositories
|
||||||
|
func (r *RepositoryConvertor) Convert(items []models.FilterItem) []models.FilterItem {
|
||||||
|
result := []models.FilterItem{}
|
||||||
|
for _, item := range items {
|
||||||
|
// just put it to the result list if the item is not a project
|
||||||
|
if item.Kind != replication.FilterItemKindProject {
|
||||||
|
result = append(result, item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories := r.registry.GetRepositories(item.Value)
|
||||||
|
for _, repository := range repositories {
|
||||||
|
result = append(result, models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: repository.Name,
|
||||||
|
Operation: item.Operation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
95
src/replication/source/repository_convertor_test.go
Normal file
95
src/replication/source/repository_convertor_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRepositoryConvert(t *testing.T) {
|
||||||
|
items := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
Value: "library",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expected := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: "library/ubuntu",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: "library/centos",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
convertor := NewRepositoryConvertor(&fakeRegistryAdaptor{})
|
||||||
|
assert.EqualValues(t, expected, convertor.Convert(items))
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRegistryAdaptor struct{}
|
||||||
|
|
||||||
|
func (f *fakeRegistryAdaptor) Kind() string {
|
||||||
|
return "fake"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRegistryAdaptor) GetNamespaces() []models.Namespace {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRegistryAdaptor) GetNamespace(name string) models.Namespace {
|
||||||
|
return models.Namespace{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRegistryAdaptor) GetRepositories(namespace string) []models.Repository {
|
||||||
|
return []models.Repository{
|
||||||
|
models.Repository{
|
||||||
|
Name: "library/ubuntu",
|
||||||
|
},
|
||||||
|
models.Repository{
|
||||||
|
Name: "library/centos",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRegistryAdaptor) GetRepository(name string, namespace string) models.Repository {
|
||||||
|
return models.Repository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRegistryAdaptor) GetTags(repositoryName string, namespace string) []models.Tag {
|
||||||
|
return []models.Tag{
|
||||||
|
models.Tag{
|
||||||
|
Name: "14.04",
|
||||||
|
},
|
||||||
|
models.Tag{
|
||||||
|
Name: "16.04",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRegistryAdaptor) GetTag(name string, repositoryName string, namespace string) models.Tag {
|
||||||
|
return models.Tag{}
|
||||||
|
}
|
89
src/replication/source/repository_filter.go
Normal file
89
src/replication/source/repository_filter.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/utils"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepositoryFilter implement Filter interface to filter repository
|
||||||
|
type RepositoryFilter struct {
|
||||||
|
pattern string
|
||||||
|
convertor Convertor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRepositoryFilter returns an instance of RepositoryFilter
|
||||||
|
func NewRepositoryFilter(pattern string, registry registry.Adaptor) *RepositoryFilter {
|
||||||
|
return &RepositoryFilter{
|
||||||
|
pattern: pattern,
|
||||||
|
convertor: NewRepositoryConvertor(registry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init ...
|
||||||
|
func (r *RepositoryFilter) Init() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConvertor ...
|
||||||
|
func (r *RepositoryFilter) GetConvertor() Convertor {
|
||||||
|
return r.convertor
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoFilter filters repository and image(according to the repository part) and drops any other resource types
|
||||||
|
func (r *RepositoryFilter) DoFilter(items []models.FilterItem) []models.FilterItem {
|
||||||
|
candidates := []string{}
|
||||||
|
for _, item := range items {
|
||||||
|
candidates = append(candidates, item.Value)
|
||||||
|
}
|
||||||
|
log.Debugf("repository filter candidates: %v", candidates)
|
||||||
|
|
||||||
|
result := []models.FilterItem{}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Kind != replication.FilterItemKindRepository && item.Kind != replication.FilterItemKindTag {
|
||||||
|
log.Warningf("unsupported type %s for repository filter, drop", item.Kind)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repository := item.Value
|
||||||
|
if item.Kind == replication.FilterItemKindTag {
|
||||||
|
repository = strings.SplitN(repository, ":", 2)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.pattern) == 0 {
|
||||||
|
log.Debugf("pattern is null, add %s to the repository filter result list", item.Value)
|
||||||
|
result = append(result, item)
|
||||||
|
} else {
|
||||||
|
// trim the project
|
||||||
|
_, repository = utils.ParseRepository(repository)
|
||||||
|
matched, err := match(r.pattern, repository)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to match pattern %s to value %s: %v", r.pattern, repository, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
log.Debugf("pattern %s matched, add %s to the repository filter result list", r.pattern, item.Value)
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
75
src/replication/source/repository_filter_test.go
Normal file
75
src/replication/source/repository_filter_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitOfRepositoryFilter(t *testing.T) {
|
||||||
|
filter := NewRepositoryFilter("", ®istry.HarborAdaptor{})
|
||||||
|
assert.Nil(t, filter.Init())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConvertorOfRepositoryFilter(t *testing.T) {
|
||||||
|
filter := NewRepositoryFilter("", ®istry.HarborAdaptor{})
|
||||||
|
assert.NotNil(t, filter.GetConvertor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoFilterOfRepositoryFilter(t *testing.T) {
|
||||||
|
// invalid filter item type
|
||||||
|
filter := NewRepositoryFilter("", ®istry.HarborAdaptor{})
|
||||||
|
items := filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: "invalid_type",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 0, len(items))
|
||||||
|
|
||||||
|
// empty pattern
|
||||||
|
filter = NewRepositoryFilter("", ®istry.HarborAdaptor{})
|
||||||
|
items = filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: "library/hello-world",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
|
||||||
|
// non-empty pattern
|
||||||
|
filter = NewRepositoryFilter("*", ®istry.HarborAdaptor{})
|
||||||
|
items = filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
|
||||||
|
// non-empty pattern
|
||||||
|
filter = NewRepositoryFilter("*", ®istry.HarborAdaptor{})
|
||||||
|
items = filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world:latest",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
}
|
36
src/replication/source/sourcer.go
Normal file
36
src/replication/source/sourcer.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Sourcer is used to manage and/or handle all the artifacts and information related with source registry.
|
||||||
|
//All the things with replication source should be covered in this object.
|
||||||
|
type Sourcer struct {
|
||||||
|
//Keep the adaptors we support now
|
||||||
|
adaptors map[string]registry.Adaptor
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewSourcer is the constructor of Sourcer
|
||||||
|
func NewSourcer() *Sourcer {
|
||||||
|
return &Sourcer{
|
||||||
|
adaptors: make(map[string]registry.Adaptor),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Init will do some initialization work like registrying all the adaptors we support
|
||||||
|
func (sc *Sourcer) Init() {
|
||||||
|
//Register Harbor adaptor
|
||||||
|
sc.adaptors[replication.AdaptorKindHarbor] = ®istry.HarborAdaptor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetAdaptor returns the required adaptor with the specified kind.
|
||||||
|
//If no adaptor with the specified kind existing, nil will be returned.
|
||||||
|
func (sc *Sourcer) GetAdaptor(kind string) registry.Adaptor {
|
||||||
|
if len(kind) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc.adaptors[kind]
|
||||||
|
}
|
24
src/replication/source/sourcer_test.go
Normal file
24
src/replication/source/sourcer_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReplicationSourcer(t *testing.T) {
|
||||||
|
testingSourcer := NewSourcer()
|
||||||
|
if testingSourcer == nil {
|
||||||
|
t.Fatal("Failed to create sourcer")
|
||||||
|
}
|
||||||
|
|
||||||
|
testingSourcer.Init()
|
||||||
|
|
||||||
|
if testingSourcer.GetAdaptor("") != nil {
|
||||||
|
t.Fatal("Empty kind should not be supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if testingSourcer.GetAdaptor(replication.AdaptorKindHarbor) == nil {
|
||||||
|
t.Fatalf("%s adaptor should be existing", replication.AdaptorKindHarbor)
|
||||||
|
}
|
||||||
|
}
|
76
src/replication/source/tag_combination_filter.go
Normal file
76
src/replication/source/tag_combination_filter.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagCombinationFilter implements Filter interface for merging tag filter items
|
||||||
|
// whose repository are same into one repository filter item
|
||||||
|
type TagCombinationFilter struct{}
|
||||||
|
|
||||||
|
// NewTagCombinationFilter returns an instance of TagCombinationFilter
|
||||||
|
func NewTagCombinationFilter() *TagCombinationFilter {
|
||||||
|
return &TagCombinationFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the filter. nil implement for now
|
||||||
|
func (t *TagCombinationFilter) Init() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConvertor returns the convertor
|
||||||
|
func (t *TagCombinationFilter) GetConvertor() Convertor {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoFilter filters resources
|
||||||
|
func (t *TagCombinationFilter) DoFilter(filterItems []models.FilterItem) []models.FilterItem {
|
||||||
|
repos := map[string][]string{}
|
||||||
|
for _, item := range filterItems {
|
||||||
|
if item.Kind != replication.FilterItemKindTag {
|
||||||
|
log.Warningf("unexpected filter item kind, expected: %s, got: %s, skip",
|
||||||
|
replication.FilterItemKindTag, item.Kind)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
strs := strings.Split(item.Value, ":")
|
||||||
|
if len(strs) != 2 {
|
||||||
|
log.Warningf("unexpected image format: %s, skip", item.Value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repos[strs[0]] = append(repos[strs[0]], strs[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO append operation
|
||||||
|
items := []models.FilterItem{}
|
||||||
|
for repo, tags := range repos {
|
||||||
|
items = append(items, models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: repo,
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"tags": tags,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
83
src/replication/source/tag_combination_filter_test.go
Normal file
83
src/replication/source/tag_combination_filter_test.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tcfilter = NewTagCombinationFilter()
|
||||||
|
|
||||||
|
func TestTagCombinationFilterInit(t *testing.T) {
|
||||||
|
assert.Nil(t, tcfilter.Init())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagCombinationFilterGetConvertor(t *testing.T) {
|
||||||
|
assert.Nil(t, tcfilter.GetConvertor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagCombinationFilterDoFilter(t *testing.T) {
|
||||||
|
items := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:invalid_tag:latest",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:14.04",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:16.04",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/centos:7",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := tcfilter.DoFilter(items)
|
||||||
|
assert.Equal(t, 2, len(result))
|
||||||
|
|
||||||
|
var ubuntu, centos models.FilterItem
|
||||||
|
if result[0].Value == "library/ubuntu" {
|
||||||
|
ubuntu = result[0]
|
||||||
|
centos = result[1]
|
||||||
|
} else {
|
||||||
|
centos = result[0]
|
||||||
|
ubuntu = result[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, replication.FilterItemKindRepository, ubuntu.Kind)
|
||||||
|
assert.Equal(t, "library/ubuntu", ubuntu.Value)
|
||||||
|
metadata, ok := ubuntu.Metadata["tags"].([]string)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.EqualValues(t, []string{"14.04", "16.04"}, metadata)
|
||||||
|
|
||||||
|
assert.Equal(t, replication.FilterItemKindRepository, centos.Kind)
|
||||||
|
assert.Equal(t, "library/centos", centos.Value)
|
||||||
|
metadata, ok = centos.Metadata["tags"].([]string)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.EqualValues(t, []string{"7"}, metadata)
|
||||||
|
}
|
55
src/replication/source/tag_convertor.go
Normal file
55
src/replication/source/tag_convertor.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagConvertor implement Convertor interface, convert repositories to tags
|
||||||
|
type TagConvertor struct {
|
||||||
|
registry registry.Adaptor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagConvertor returns an instance of TagConvertor
|
||||||
|
func NewTagConvertor(registry registry.Adaptor) *TagConvertor {
|
||||||
|
return &TagConvertor{
|
||||||
|
registry: registry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Convert repositories to tags
|
||||||
|
func (t *TagConvertor) Convert(items []models.FilterItem) []models.FilterItem {
|
||||||
|
result := []models.FilterItem{}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Kind != replication.FilterItemKindRepository {
|
||||||
|
// just put it to the result list if the item is not a repository
|
||||||
|
result = append(result, item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := t.registry.GetTags(item.Value, "")
|
||||||
|
for _, tag := range tags {
|
||||||
|
result = append(result, models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: item.Value + ":" + tag.Name,
|
||||||
|
Operation: item.Operation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
51
src/replication/source/tag_convertor_test.go
Normal file
51
src/replication/source/tag_convertor_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTagConvert(t *testing.T) {
|
||||||
|
items := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindRepository,
|
||||||
|
Value: "library/ubuntu",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expected := []models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:14.04",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/ubuntu:16.04",
|
||||||
|
},
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindProject,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
convertor := NewTagConvertor(&fakeRegistryAdaptor{})
|
||||||
|
assert.EqualValues(t, expected, convertor.Convert(items))
|
||||||
|
}
|
84
src/replication/source/tag_filter.go
Normal file
84
src/replication/source/tag_filter.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagFilter implements Filter interface to filter tag
|
||||||
|
type TagFilter struct {
|
||||||
|
pattern string
|
||||||
|
convertor Convertor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagFilter returns an instance of TagFilter
|
||||||
|
func NewTagFilter(pattern string, registry registry.Adaptor) *TagFilter {
|
||||||
|
return &TagFilter{
|
||||||
|
pattern: pattern,
|
||||||
|
convertor: NewTagConvertor(registry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init ...
|
||||||
|
func (t *TagFilter) Init() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConvertor ...
|
||||||
|
func (t *TagFilter) GetConvertor() Convertor {
|
||||||
|
return t.convertor
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoFilter filters tag of the image
|
||||||
|
func (t *TagFilter) DoFilter(items []models.FilterItem) []models.FilterItem {
|
||||||
|
candidates := []string{}
|
||||||
|
for _, item := range items {
|
||||||
|
candidates = append(candidates, item.Value)
|
||||||
|
}
|
||||||
|
log.Debugf("tag filter candidates: %v", candidates)
|
||||||
|
|
||||||
|
result := []models.FilterItem{}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Kind != replication.FilterItemKindTag {
|
||||||
|
log.Warningf("unsupported type %s for tag filter, dropped", item.Kind)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.pattern) == 0 {
|
||||||
|
log.Debugf("pattern is null, add %s to the tag filter result list", item.Value)
|
||||||
|
result = append(result, item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := strings.SplitN(item.Value, ":", 2)[1]
|
||||||
|
matched, err := match(t.pattern, tag)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to match pattern %s to value %s: %v", t.pattern, tag, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
log.Debugf("pattern %s matched, add %s to the tag filter result list", t.pattern, item.Value)
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
85
src/replication/source/tag_filter_test.go
Normal file
85
src/replication/source/tag_filter_test.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
"github.com/vmware/harbor/src/replication/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitOfTagFilter(t *testing.T) {
|
||||||
|
filter := NewTagFilter("", ®istry.HarborAdaptor{})
|
||||||
|
assert.Nil(t, filter.Init())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConvertorOfTagFilter(t *testing.T) {
|
||||||
|
filter := NewTagFilter("", ®istry.HarborAdaptor{})
|
||||||
|
assert.NotNil(t, filter.GetConvertor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoFilterOfTagFilter(t *testing.T) {
|
||||||
|
// invalid filter item type
|
||||||
|
filter := NewTagFilter("", ®istry.HarborAdaptor{})
|
||||||
|
items := filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: "invalid_type",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 0, len(items))
|
||||||
|
|
||||||
|
// empty pattern
|
||||||
|
filter = NewTagFilter("", ®istry.HarborAdaptor{})
|
||||||
|
items = filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world:latest",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
|
||||||
|
// non-empty pattern
|
||||||
|
filter = NewTagFilter("l*t", ®istry.HarborAdaptor{})
|
||||||
|
items = filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world:latest",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
|
||||||
|
// non-empty pattern
|
||||||
|
filter = NewTagFilter("lates?", ®istry.HarborAdaptor{})
|
||||||
|
items = filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world:latest",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
|
||||||
|
// non-empty pattern
|
||||||
|
filter = NewTagFilter("latest?", ®istry.HarborAdaptor{})
|
||||||
|
items = filter.DoFilter([]models.FilterItem{
|
||||||
|
models.FilterItem{
|
||||||
|
Kind: replication.FilterItemKindTag,
|
||||||
|
Value: "library/hello-world:latest",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Equal(t, 0, len(items))
|
||||||
|
}
|
38
src/replication/target/target.go
Normal file
38
src/replication/target/target.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager defines the methods that a target manager should implement
|
||||||
|
type Manager interface {
|
||||||
|
GetTarget(int64) (*models.RepTarget, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultManager implement the Manager interface
|
||||||
|
type DefaultManager struct{}
|
||||||
|
|
||||||
|
// NewDefaultManager returns an instance of DefaultManger
|
||||||
|
func NewDefaultManager() *DefaultManager {
|
||||||
|
return &DefaultManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTarget ...
|
||||||
|
func (d *DefaultManager) GetTarget(id int64) (*models.RepTarget, error) {
|
||||||
|
return dao.GetRepTarget(id)
|
||||||
|
}
|
26
src/replication/target/target_test.go
Normal file
26
src/replication/target/target_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDefaultManager(t *testing.T) {
|
||||||
|
mgr := NewDefaultManager()
|
||||||
|
assert.NotNil(t, mgr)
|
||||||
|
}
|
212
src/replication/trigger/cache.go
Normal file
212
src/replication/trigger/cache.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/heap"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//The max count of items the cache can keep
|
||||||
|
defaultCapacity = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
//Item keeps more metadata of the triggers which are stored in the heap.
|
||||||
|
type Item struct {
|
||||||
|
//Which policy the trigger belong to
|
||||||
|
policyID int64
|
||||||
|
|
||||||
|
//Frequency of cache querying
|
||||||
|
//First compration factor
|
||||||
|
frequency int
|
||||||
|
|
||||||
|
//The timestamp of being put into heap
|
||||||
|
//Second compration factor
|
||||||
|
timestamp int64
|
||||||
|
|
||||||
|
//The index in the heap
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
//MetaQueue implements heap.Interface and holds items which are metadata of trigger
|
||||||
|
type MetaQueue []*Item
|
||||||
|
|
||||||
|
//Len return the size of the queue
|
||||||
|
func (mq MetaQueue) Len() int {
|
||||||
|
return len(mq)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Less is a comparator of heap
|
||||||
|
func (mq MetaQueue) Less(i, j int) bool {
|
||||||
|
return mq[i].frequency < mq[j].frequency ||
|
||||||
|
(mq[i].frequency == mq[j].frequency &&
|
||||||
|
mq[i].timestamp < mq[j].timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Swap the items to rebuild heap
|
||||||
|
func (mq MetaQueue) Swap(i, j int) {
|
||||||
|
mq[i], mq[j] = mq[j], mq[i]
|
||||||
|
mq[i].index = i
|
||||||
|
mq[j].index = j
|
||||||
|
}
|
||||||
|
|
||||||
|
//Push item into heap
|
||||||
|
func (mq *MetaQueue) Push(x interface{}) {
|
||||||
|
item := x.(*Item)
|
||||||
|
n := len(*mq)
|
||||||
|
item.index = n
|
||||||
|
item.timestamp = time.Now().UTC().UnixNano()
|
||||||
|
*mq = append(*mq, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Pop smallest item from heap
|
||||||
|
func (mq *MetaQueue) Pop() interface{} {
|
||||||
|
old := *mq
|
||||||
|
n := len(old)
|
||||||
|
item := old[n-1] //Smallest item
|
||||||
|
item.index = -1 //For safety
|
||||||
|
*mq = old[:n-1]
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update the frequency of item
|
||||||
|
func (mq *MetaQueue) Update(item *Item) {
|
||||||
|
item.frequency++
|
||||||
|
heap.Fix(mq, item.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
//CacheItem is the data stored in the cache.
|
||||||
|
//It contains trigger and heap item references.
|
||||||
|
type CacheItem struct {
|
||||||
|
//The trigger reference
|
||||||
|
trigger Interface
|
||||||
|
|
||||||
|
//The heap item reference
|
||||||
|
item *Item
|
||||||
|
}
|
||||||
|
|
||||||
|
//Cache is used to cache the enabled triggers with specified capacity.
|
||||||
|
//If exceed the capacity, cached items will be adjusted with the following rules:
|
||||||
|
// The item with least usage frequency will be replaced;
|
||||||
|
// If multiple items with same usage frequency, the oldest one will be replaced.
|
||||||
|
type Cache struct {
|
||||||
|
//The max count of items this cache can keep
|
||||||
|
capacity int
|
||||||
|
|
||||||
|
//Lock to handle concurrent case
|
||||||
|
lock *sync.RWMutex
|
||||||
|
|
||||||
|
//Hash map for quick locating cached item
|
||||||
|
hash map[string]CacheItem
|
||||||
|
|
||||||
|
//Heap for quick locating the trigger with least usage
|
||||||
|
queue *MetaQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewCache is constructor of cache
|
||||||
|
func NewCache(capacity int) *Cache {
|
||||||
|
cap := capacity
|
||||||
|
if cap <= 0 {
|
||||||
|
cap = defaultCapacity
|
||||||
|
}
|
||||||
|
|
||||||
|
//Initialize heap
|
||||||
|
mq := make(MetaQueue, 0)
|
||||||
|
heap.Init(&mq)
|
||||||
|
|
||||||
|
return &Cache{
|
||||||
|
capacity: cap,
|
||||||
|
lock: new(sync.RWMutex),
|
||||||
|
hash: make(map[string]CacheItem),
|
||||||
|
queue: &mq,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the trigger interface with the specified policy ID
|
||||||
|
func (c *Cache) Get(policyID int64) Interface {
|
||||||
|
if policyID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lock.RLock()
|
||||||
|
defer c.lock.RUnlock()
|
||||||
|
|
||||||
|
k := c.key(policyID)
|
||||||
|
|
||||||
|
if cacheItem, ok := c.hash[k]; ok {
|
||||||
|
//Update frequency
|
||||||
|
c.queue.Update(cacheItem.item)
|
||||||
|
return cacheItem.trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Put the item into cache with ID of ploicy as key
|
||||||
|
func (c *Cache) Put(policyID int64, trigger Interface) {
|
||||||
|
if policyID <= 0 || trigger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
//Exceed the capacity?
|
||||||
|
if c.Size() >= c.capacity {
|
||||||
|
//Pop one for the new one
|
||||||
|
v := heap.Pop(c.queue)
|
||||||
|
item := v.(*Item)
|
||||||
|
//Remove from hash
|
||||||
|
delete(c.hash, c.key(item.policyID))
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add to meta queue
|
||||||
|
item := &Item{
|
||||||
|
policyID: policyID,
|
||||||
|
frequency: 1,
|
||||||
|
}
|
||||||
|
heap.Push(c.queue, item)
|
||||||
|
|
||||||
|
//Cache
|
||||||
|
cacheItem := CacheItem{
|
||||||
|
trigger: trigger,
|
||||||
|
item: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
k := c.key(policyID)
|
||||||
|
c.hash[k] = cacheItem
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove the trigger attached to the specified policy
|
||||||
|
func (c *Cache) Remove(policyID int64) Interface {
|
||||||
|
if policyID > 0 {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
//If existing
|
||||||
|
k := c.key(policyID)
|
||||||
|
if cacheItem, ok := c.hash[k]; ok {
|
||||||
|
//Remove from heap
|
||||||
|
heap.Remove(c.queue, cacheItem.item.index)
|
||||||
|
|
||||||
|
//Remove from hash
|
||||||
|
delete(c.hash, k)
|
||||||
|
|
||||||
|
return cacheItem.trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Size return the count of triggers in the cache
|
||||||
|
func (c *Cache) Size() int {
|
||||||
|
return len(c.hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Generate a hash key with the policy ID
|
||||||
|
func (c *Cache) key(policyID int64) string {
|
||||||
|
return fmt.Sprintf("trigger-%d", policyID)
|
||||||
|
}
|
53
src/replication/trigger/cache_test.go
Normal file
53
src/replication/trigger/cache_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func TestCache(t *testing.T) {
|
||||||
|
cache := NewCache(10)
|
||||||
|
trigger := NewImmediateTrigger(ImmediateParam{})
|
||||||
|
|
||||||
|
cache.Put(1, trigger)
|
||||||
|
if cache.Size() != 1 {
|
||||||
|
t.Fatalf("Invalid size, expect 1 but got %d", cache.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := cache.Get(1)
|
||||||
|
if tr == nil {
|
||||||
|
t.Fatal("Should not get nil item")
|
||||||
|
}
|
||||||
|
|
||||||
|
tri := cache.Remove(1)
|
||||||
|
if tri == nil || cache.Size() > 0 {
|
||||||
|
t.Fatal("Failed to remove")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheChange(t *testing.T) {
|
||||||
|
cache := NewCache(2)
|
||||||
|
trigger1 := NewImmediateTrigger(ImmediateParam{})
|
||||||
|
trigger2 := NewImmediateTrigger(ImmediateParam{})
|
||||||
|
cache.Put(1, trigger1)
|
||||||
|
cache.Put(2, trigger2)
|
||||||
|
|
||||||
|
if cache.Size() != 2 {
|
||||||
|
t.Fatalf("Invalid size, expect 2 but got %d", cache.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tr := cache.Get(2); tr == nil {
|
||||||
|
t.Fatal("Should not get nil item")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Microsecond)
|
||||||
|
|
||||||
|
trigger3 := NewImmediateTrigger(ImmediateParam{})
|
||||||
|
cache.Put(3, trigger3)
|
||||||
|
if cache.Size() != 2 {
|
||||||
|
t.Fatalf("Invalid size, expect 2 but got %d", cache.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tr := cache.Get(1); tr != nil {
|
||||||
|
t.Fatal("item1 should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
46
src/replication/trigger/immediate.go
Normal file
46
src/replication/trigger/immediate.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
//ImmediateTrigger will setup watcher at the image pushing action to fire
|
||||||
|
//replication event at pushing happening time.
|
||||||
|
type ImmediateTrigger struct {
|
||||||
|
params ImmediateParam
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewImmediateTrigger is constructor of ImmediateTrigger
|
||||||
|
func NewImmediateTrigger(params ImmediateParam) *ImmediateTrigger {
|
||||||
|
return &ImmediateTrigger{
|
||||||
|
params: params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Kind is the implementation of same method defined in Trigger interface
|
||||||
|
func (st *ImmediateTrigger) Kind() string {
|
||||||
|
return replication.TriggerKindImmediate
|
||||||
|
}
|
||||||
|
|
||||||
|
//Setup is the implementation of same method defined in Trigger interface
|
||||||
|
func (st *ImmediateTrigger) Setup() error {
|
||||||
|
//TODO: Need more complicated logic here to handle partial updates
|
||||||
|
for _, namespace := range st.params.Namespaces {
|
||||||
|
wt := WatchItem{
|
||||||
|
PolicyID: st.params.PolicyID,
|
||||||
|
Namespace: namespace,
|
||||||
|
OnDeletion: st.params.OnDeletion,
|
||||||
|
OnPush: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DefaultWatchList.Add(wt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Unset is the implementation of same method defined in Trigger interface
|
||||||
|
func (st *ImmediateTrigger) Unset() error {
|
||||||
|
return DefaultWatchList.Remove(st.params.PolicyID)
|
||||||
|
}
|
57
src/replication/trigger/immediate_test.go
Normal file
57
src/replication/trigger/immediate_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/test"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKindOfImmediateTrigger(t *testing.T) {
|
||||||
|
trigger := NewImmediateTrigger(ImmediateParam{})
|
||||||
|
assert.Equal(t, replication.TriggerKindImmediate, trigger.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupAndUnsetOfImmediateTrigger(t *testing.T) {
|
||||||
|
dao.DefaultDatabaseWatchItemDAO = &test.FakeWatchItemDAO{}
|
||||||
|
|
||||||
|
param := ImmediateParam{}
|
||||||
|
param.PolicyID = 1
|
||||||
|
param.OnDeletion = true
|
||||||
|
param.Namespaces = []string{"library"}
|
||||||
|
trigger := NewImmediateTrigger(param)
|
||||||
|
|
||||||
|
err := trigger.Setup()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
items, err := DefaultWatchList.Get("library", "push")
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
|
||||||
|
items, err = DefaultWatchList.Get("library", "delete")
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(items))
|
||||||
|
|
||||||
|
err = trigger.Unset()
|
||||||
|
require.Nil(t, err)
|
||||||
|
items, err = DefaultWatchList.Get("library", "delete")
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(items))
|
||||||
|
}
|
13
src/replication/trigger/interface.go
Normal file
13
src/replication/trigger/interface.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
//Interface is certian mechanism to know when fire the replication operation.
|
||||||
|
type Interface interface {
|
||||||
|
//Kind indicates what type of the trigger is.
|
||||||
|
Kind() string
|
||||||
|
|
||||||
|
//Setup/enable the trigger; if failed, an error would be returned.
|
||||||
|
Setup() error
|
||||||
|
|
||||||
|
//Remove/disable the trigger; if failed, an error would be returned.
|
||||||
|
Unset() error
|
||||||
|
}
|
124
src/replication/trigger/manager.go
Normal file
124
src/replication/trigger/manager.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Manager provides unified methods to manage the triggers of policies;
|
||||||
|
//Cache the enabled triggers, setup/unset the trigger based on the parameters
|
||||||
|
//with json format.
|
||||||
|
type Manager struct {
|
||||||
|
//Cache for triggers
|
||||||
|
//cache *Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewManager is the constructor of trigger manager.
|
||||||
|
//capacity is the max number of trigger references manager can keep in memory
|
||||||
|
func NewManager(capacity int) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
//cache: NewCache(capacity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
//GetTrigger returns the enabled trigger reference if existing in the cache.
|
||||||
|
func (m *Manager) GetTrigger(policyID int64) Interface {
|
||||||
|
return m.cache.Get(policyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
//RemoveTrigger will disable the trigger and remove it from the cache if existing.
|
||||||
|
func (m *Manager) RemoveTrigger(policyID int64) error {
|
||||||
|
trigger := m.cache.Get(policyID)
|
||||||
|
if trigger == nil {
|
||||||
|
return errors.New("Trigger is not cached, please use UnsetTrigger to disable the trigger")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Unset trigger
|
||||||
|
if err := trigger.Unset(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove from cache
|
||||||
|
//No need to check the return of remove because the dirty item cached in the cache
|
||||||
|
//will be removed out finally after a certain while
|
||||||
|
m.cache.Remove(policyID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
//SetupTrigger will create the new trigger based on the provided policy.
|
||||||
|
//If failed, an error will be returned.
|
||||||
|
func (m *Manager) SetupTrigger(policy *models.ReplicationPolicy) error {
|
||||||
|
trigger, err := createTrigger(policy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// manual trigger, do nothing
|
||||||
|
if trigger == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tg := trigger.(Interface)
|
||||||
|
if err = tg.Setup(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("%s trigger for policy %d is set", tg.Kind(), policy.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//UnsetTrigger will disable the trigger which is not cached in the trigger cache.
|
||||||
|
func (m *Manager) UnsetTrigger(policy *models.ReplicationPolicy) error {
|
||||||
|
trigger, err := createTrigger(policy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// manual trigger, do nothing
|
||||||
|
if trigger == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tg := trigger.(Interface)
|
||||||
|
if err = tg.Unset(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("%s trigger for policy %d is unset", tg.Kind(), policy.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTrigger(policy *models.ReplicationPolicy) (interface{}, error) {
|
||||||
|
if policy == nil || policy.Trigger == nil {
|
||||||
|
return nil, fmt.Errorf("empty policy or trigger")
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger := policy.Trigger
|
||||||
|
switch trigger.Kind {
|
||||||
|
case replication.TriggerKindSchedule:
|
||||||
|
param := ScheduleParam{}
|
||||||
|
param.PolicyID = policy.ID
|
||||||
|
param.Type = trigger.ScheduleParam.Type
|
||||||
|
param.Weekday = trigger.ScheduleParam.Weekday
|
||||||
|
param.Offtime = trigger.ScheduleParam.Offtime
|
||||||
|
|
||||||
|
return NewScheduleTrigger(param), nil
|
||||||
|
case replication.TriggerKindImmediate:
|
||||||
|
param := ImmediateParam{}
|
||||||
|
param.PolicyID = policy.ID
|
||||||
|
param.OnDeletion = policy.ReplicateDeletion
|
||||||
|
param.Namespaces = policy.Namespaces
|
||||||
|
|
||||||
|
return NewImmediateTrigger(param), nil
|
||||||
|
case replication.TriggerKindManual:
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid trigger type: %s", trigger.Kind)
|
||||||
|
}
|
||||||
|
}
|
96
src/replication/trigger/manager_test.go
Normal file
96
src/replication/trigger/manager_test.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateTrigger(t *testing.T) {
|
||||||
|
// nil policy
|
||||||
|
_, err := createTrigger(nil)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// nil trigger
|
||||||
|
_, err = createTrigger(&models.ReplicationPolicy{})
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// schedule trigger
|
||||||
|
trigger, err := createTrigger(&models.ReplicationPolicy{
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindSchedule,
|
||||||
|
ScheduleParam: &models.ScheduleParam{
|
||||||
|
Type: replication.TriggerScheduleWeekly,
|
||||||
|
Weekday: 1,
|
||||||
|
Offtime: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.NotNil(t, trigger)
|
||||||
|
|
||||||
|
// immediate trigger
|
||||||
|
trigger, err = createTrigger(&models.ReplicationPolicy{
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindImmediate,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.NotNil(t, trigger)
|
||||||
|
|
||||||
|
// manual trigger
|
||||||
|
trigger, err = createTrigger(&models.ReplicationPolicy{
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindManual,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Nil(t, trigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupTrigger(t *testing.T) {
|
||||||
|
mgr := NewManager(1)
|
||||||
|
|
||||||
|
err := mgr.SetupTrigger(&models.ReplicationPolicy{
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindSchedule,
|
||||||
|
ScheduleParam: &models.ScheduleParam{
|
||||||
|
Type: replication.TriggerScheduleDaily,
|
||||||
|
Offtime: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnsetTrigger(t *testing.T) {
|
||||||
|
mgr := NewManager(1)
|
||||||
|
|
||||||
|
err := mgr.UnsetTrigger(&models.ReplicationPolicy{
|
||||||
|
Trigger: &models.Trigger{
|
||||||
|
Kind: replication.TriggerKindSchedule,
|
||||||
|
ScheduleParam: &models.ScheduleParam{
|
||||||
|
Type: replication.TriggerScheduleDaily,
|
||||||
|
Offtime: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
22
src/replication/trigger/param_immediate.go
Normal file
22
src/replication/trigger/param_immediate.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
//NOTES: Whether replicate the existing images when the type of trigger is
|
||||||
|
//'Immediate' is a once-effective setting which will not be persisted
|
||||||
|
// and kept as one parameter of 'Immediate' trigger. It will only be
|
||||||
|
//covered by the UI logic.
|
||||||
|
|
||||||
|
//ImmediateParam defines the parameter of immediate trigger
|
||||||
|
type ImmediateParam struct {
|
||||||
|
//Basic parameters
|
||||||
|
BasicParam
|
||||||
|
|
||||||
|
//Namepaces
|
||||||
|
Namespaces []string
|
||||||
|
}
|
||||||
|
|
||||||
|
//Parse is the implementation of same method in TriggerParam interface
|
||||||
|
//NOTES: No need to implement this method for 'Immediate' trigger as
|
||||||
|
//it does not have any parameters with json format.
|
||||||
|
func (ip ImmediateParam) Parse(param string) error {
|
||||||
|
return nil
|
||||||
|
}
|
30
src/replication/trigger/param_schedule.go
Normal file
30
src/replication/trigger/param_schedule.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//ScheduleParam defines the parameter of schedule trigger
|
||||||
|
type ScheduleParam struct {
|
||||||
|
//Basic parameters
|
||||||
|
BasicParam
|
||||||
|
|
||||||
|
//Daily or weekly
|
||||||
|
Type string
|
||||||
|
|
||||||
|
//Optional, only used when type is 'weekly'
|
||||||
|
Weekday int8
|
||||||
|
|
||||||
|
//The time offset with the UTC 00:00 in seconds
|
||||||
|
Offtime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
//Parse is the implementation of same method in TriggerParam interface
|
||||||
|
func (stp ScheduleParam) Parse(param string) error {
|
||||||
|
if len(param) == 0 {
|
||||||
|
return errors.New("Parameter of schedule trigger should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal([]byte(param), &stp)
|
||||||
|
}
|
58
src/replication/trigger/schedule.go
Normal file
58
src/replication/trigger/schedule.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/scheduler"
|
||||||
|
"github.com/vmware/harbor/src/common/scheduler/policy"
|
||||||
|
replication_task "github.com/vmware/harbor/src/common/scheduler/task/replication"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
//ScheduleTrigger will schedule a alternate policy to provide 'daily' and 'weekly' trigger ways.
|
||||||
|
type ScheduleTrigger struct {
|
||||||
|
params ScheduleParam
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewScheduleTrigger is constructor of ScheduleTrigger
|
||||||
|
func NewScheduleTrigger(params ScheduleParam) *ScheduleTrigger {
|
||||||
|
return &ScheduleTrigger{
|
||||||
|
params: params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Kind is the implementation of same method defined in Trigger interface
|
||||||
|
func (st *ScheduleTrigger) Kind() string {
|
||||||
|
return replication.TriggerKindSchedule
|
||||||
|
}
|
||||||
|
|
||||||
|
//Setup is the implementation of same method defined in Trigger interface
|
||||||
|
func (st *ScheduleTrigger) Setup() error {
|
||||||
|
config := &policy.AlternatePolicyConfiguration{}
|
||||||
|
switch st.params.Type {
|
||||||
|
case replication.TriggerScheduleDaily:
|
||||||
|
config.Duration = 24 * 3600 * time.Second
|
||||||
|
config.OffsetTime = st.params.Offtime
|
||||||
|
case replication.TriggerScheduleWeekly:
|
||||||
|
config.Duration = 7 * 24 * 3600 * time.Second
|
||||||
|
config.OffsetTime = st.params.Offtime
|
||||||
|
config.Weekday = st.params.Weekday
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported schedual trigger type: %s", st.params.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulePolicy := policy.NewAlternatePolicy(assembleName(st.params.PolicyID), config)
|
||||||
|
attachTask := replication_task.NewTask(st.params.PolicyID)
|
||||||
|
schedulePolicy.AttachTasks(attachTask)
|
||||||
|
return scheduler.DefaultScheduler.Schedule(schedulePolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Unset is the implementation of same method defined in Trigger interface
|
||||||
|
func (st *ScheduleTrigger) Unset() error {
|
||||||
|
return scheduler.DefaultScheduler.UnSchedule(assembleName(st.params.PolicyID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func assembleName(policyID int64) string {
|
||||||
|
return fmt.Sprintf("replication_policy_%d", policyID)
|
||||||
|
}
|
63
src/replication/trigger/schedule_test.go
Normal file
63
src/replication/trigger/schedule_test.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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 trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/vmware/harbor/src/common/scheduler"
|
||||||
|
"github.com/vmware/harbor/src/replication"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAssembleName(t *testing.T) {
|
||||||
|
assert.Equal(t, "replication_policy_1", assembleName(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKindOfScheduleTrigger(t *testing.T) {
|
||||||
|
trigger := NewScheduleTrigger(ScheduleParam{})
|
||||||
|
assert.Equal(t, replication.TriggerKindSchedule, trigger.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupAndUnSetOfScheduleTrigger(t *testing.T) {
|
||||||
|
// invalid schedule param
|
||||||
|
trigger := NewScheduleTrigger(ScheduleParam{})
|
||||||
|
assert.NotNil(t, trigger.Setup())
|
||||||
|
|
||||||
|
// valid schedule param
|
||||||
|
var policyID int64 = 1
|
||||||
|
trigger = NewScheduleTrigger(ScheduleParam{
|
||||||
|
BasicParam: BasicParam{
|
||||||
|
PolicyID: policyID,
|
||||||
|
},
|
||||||
|
Type: replication.TriggerScheduleWeekly,
|
||||||
|
Weekday: (int8(time.Now().Weekday()) + 1) % 7,
|
||||||
|
Offtime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
count := scheduler.DefaultScheduler.PolicyCount()
|
||||||
|
require.Nil(t, scheduler.DefaultScheduler.GetPolicy(assembleName(policyID)))
|
||||||
|
|
||||||
|
require.Nil(t, trigger.Setup())
|
||||||
|
|
||||||
|
assert.Equal(t, count+1, scheduler.DefaultScheduler.PolicyCount())
|
||||||
|
assert.NotNil(t, scheduler.DefaultScheduler.GetPolicy(assembleName(policyID)))
|
||||||
|
|
||||||
|
require.Nil(t, trigger.Unset())
|
||||||
|
assert.Equal(t, count, scheduler.DefaultScheduler.PolicyCount())
|
||||||
|
assert.Nil(t, scheduler.DefaultScheduler.GetPolicy(assembleName(policyID)))
|
||||||
|
}
|
17
src/replication/trigger/trigger_param.go
Normal file
17
src/replication/trigger/trigger_param.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
//BasicParam contains the general parameters for all triggers
|
||||||
|
type BasicParam struct {
|
||||||
|
//ID of the related policy
|
||||||
|
PolicyID int64
|
||||||
|
|
||||||
|
//Whether delete remote replicated images if local ones are deleted
|
||||||
|
OnDeletion bool
|
||||||
|
}
|
||||||
|
|
||||||
|
//Parameter defines operation of doing initialization from parameter json text
|
||||||
|
type Parameter interface {
|
||||||
|
//Decode parameter with json style to the owner struct
|
||||||
|
//If failed, an error will be returned
|
||||||
|
Parse(param string) error
|
||||||
|
}
|
65
src/replication/trigger/watch_list.go
Normal file
65
src/replication/trigger/watch_list.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
//DefaultWatchList is the default instance of WatchList
|
||||||
|
var DefaultWatchList = &WatchList{}
|
||||||
|
|
||||||
|
//WatchList contains the items which should be evaluated for replication
|
||||||
|
//when image pushing or deleting happens.
|
||||||
|
type WatchList struct{}
|
||||||
|
|
||||||
|
//WatchItem keeps the related data for evaluation in WatchList.
|
||||||
|
type WatchItem struct {
|
||||||
|
//ID of policy
|
||||||
|
PolicyID int64
|
||||||
|
|
||||||
|
//Corresponding namespace
|
||||||
|
Namespace string
|
||||||
|
|
||||||
|
//For deletion event
|
||||||
|
OnDeletion bool
|
||||||
|
|
||||||
|
//For pushing event
|
||||||
|
OnPush bool
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add item to the list and persist into DB
|
||||||
|
func (wl *WatchList) Add(item WatchItem) error {
|
||||||
|
_, err := dao.DefaultDatabaseWatchItemDAO.Add(
|
||||||
|
&models.WatchItem{
|
||||||
|
PolicyID: item.PolicyID,
|
||||||
|
Namespace: item.Namespace,
|
||||||
|
OnPush: item.OnPush,
|
||||||
|
OnDeletion: item.OnDeletion,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove the specified watch item from list
|
||||||
|
func (wl *WatchList) Remove(policyID int64) error {
|
||||||
|
return dao.DefaultDatabaseWatchItemDAO.DeleteByPolicyID(policyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the watch items according to the namespace and operation
|
||||||
|
func (wl *WatchList) Get(namespace, operation string) ([]WatchItem, error) {
|
||||||
|
items, err := dao.DefaultDatabaseWatchItemDAO.Get(namespace, operation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
watchItems := []WatchItem{}
|
||||||
|
for _, item := range items {
|
||||||
|
watchItems = append(watchItems, WatchItem{
|
||||||
|
PolicyID: item.PolicyID,
|
||||||
|
Namespace: item.Namespace,
|
||||||
|
OnPush: item.OnPush,
|
||||||
|
OnDeletion: item.OnDeletion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return watchItems, nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user