mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-25 19:56:09 +01:00
Refeactor replication policy APIs
Refeactor replication policy APIs Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
parent
7694c131a4
commit
3d7fd070c7
@ -738,172 +738,6 @@ paths:
|
||||
description: The auth mode of the system is not "oidc_auth", or the user is not onboarded via OIDC AuthN.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/replication/policies:
|
||||
get:
|
||||
summary: List replication policies
|
||||
description: |
|
||||
This endpoint let user list replication policies
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: The replication policy name.
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: The page number.
|
||||
- name: page_size
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: The size of per page.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get policy successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of available items
|
||||
type: integer
|
||||
Link:
|
||||
description: Link to previous page and next page
|
||||
type: string
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
post:
|
||||
summary: Create a replication policy
|
||||
description: |
|
||||
This endpoint let user create a replication policy
|
||||
parameters:
|
||||
- name: policy
|
||||
in: body
|
||||
description: The policy model.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'201':
|
||||
$ref: '#/responses/Created'
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'409':
|
||||
$ref: '#/responses/Conflict'
|
||||
'415':
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
'/replication/policies/{id}':
|
||||
get:
|
||||
summary: Get replication policy.
|
||||
description: |
|
||||
This endpoint let user get replication policy by specific ID.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: policy ID
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get the replication policy successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
put:
|
||||
summary: Update the replication policy
|
||||
description: |
|
||||
This endpoint let user update policy.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: policy ID
|
||||
- name: policy
|
||||
in: body
|
||||
description: The replication policy model.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/OK'
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/responses/Conflict'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
delete:
|
||||
summary: Delete the replication policy specified by ID.
|
||||
description: |
|
||||
Delete the replication policy specified by ID.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Replication policy ID
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/OK'
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/responses/NotFound'
|
||||
'412':
|
||||
$ref: '#/responses/PreconditionFailed'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
/labels:
|
||||
get:
|
||||
summary: List labels according to the query strings.
|
||||
@ -2205,73 +2039,6 @@ definitions:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 'The count of the total repositories, only be seen when the user is admin.'
|
||||
ReplicationPolicy:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The policy ID.
|
||||
name:
|
||||
type: string
|
||||
description: The policy name.
|
||||
description:
|
||||
type: string
|
||||
description: The description of the policy.
|
||||
src_registry:
|
||||
description: The source registry.
|
||||
$ref: '#/definitions/Registry'
|
||||
dest_registry:
|
||||
description: The destination registry.
|
||||
$ref: '#/definitions/Registry'
|
||||
dest_namespace:
|
||||
type: string
|
||||
description: The destination namespace.
|
||||
trigger:
|
||||
$ref: '#/definitions/ReplicationTrigger'
|
||||
filters:
|
||||
type: array
|
||||
description: The replication policy filter array.
|
||||
items:
|
||||
$ref: '#/definitions/ReplicationFilter'
|
||||
deletion:
|
||||
type: boolean
|
||||
description: Whether to replicate the deletion operation.
|
||||
override:
|
||||
type: boolean
|
||||
description: Whether to override the resources on the destination registry.
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether the policy is enabled or not.
|
||||
creation_time:
|
||||
type: string
|
||||
description: The create time of the policy.
|
||||
update_time:
|
||||
type: string
|
||||
description: The update time of the policy.
|
||||
ReplicationTrigger:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 'The replication policy trigger type. The valid values are manual, event_based and scheduled.'
|
||||
trigger_settings:
|
||||
$ref: '#/definitions/TriggerSettings'
|
||||
TriggerSettings:
|
||||
type: object
|
||||
properties:
|
||||
cron:
|
||||
type: string
|
||||
description: The cron string for scheduled trigger
|
||||
ReplicationFilter:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 'The replication policy filter type.'
|
||||
value:
|
||||
type: string
|
||||
description: 'The value of replication policy filter.'
|
||||
RegistryCredential:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -2255,6 +2255,152 @@ paths:
|
||||
$ref: '#/responses/404'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/replication/policies:
|
||||
get:
|
||||
summary: List replication policies
|
||||
description: List replication policies
|
||||
tags:
|
||||
- replication
|
||||
operationId: listReplicationPolicies
|
||||
parameters:
|
||||
- $ref: '#/parameters/query'
|
||||
- $ref: '#/parameters/sort'
|
||||
- $ref: '#/parameters/page'
|
||||
- $ref: '#/parameters/pageSize'
|
||||
- name: name
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: Deprecated, use "query" instead. The policy name.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of the resources
|
||||
type: integer
|
||||
Link:
|
||||
description: Link refers to the previous page and next page
|
||||
type: string
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
post:
|
||||
summary: Create a replication policy
|
||||
description: Create a replication policy
|
||||
tags:
|
||||
- replication
|
||||
operationId: createReplicationPolicy
|
||||
parameters:
|
||||
- name: policy
|
||||
in: body
|
||||
description: The replication policy
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
responses:
|
||||
'201':
|
||||
$ref: '#/responses/201'
|
||||
'400':
|
||||
$ref: '#/responses/400'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'409':
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/replication/policies/{id}:
|
||||
get:
|
||||
summary: Get the specific replication policy
|
||||
description: Get the specific replication policy
|
||||
tags:
|
||||
- replication
|
||||
operationId: getReplicationPolicy
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Policy ID
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
delete:
|
||||
summary: Delete the specific replication policy
|
||||
description: Delete the specific replication policy
|
||||
tags:
|
||||
- replication
|
||||
operationId: deleteReplicationPolicy
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Replication policy ID
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'412':
|
||||
$ref: '#/responses/412'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
put:
|
||||
summary: Update the replication policy
|
||||
description: Update the replication policy
|
||||
tags:
|
||||
- replication
|
||||
operationId: updateReplicationPolicy
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The policy ID
|
||||
- name: policy
|
||||
in: body
|
||||
description: The replication policy
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/200'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
'403':
|
||||
$ref: '#/responses/403'
|
||||
'404':
|
||||
$ref: '#/responses/404'
|
||||
'409':
|
||||
$ref: '#/responses/409'
|
||||
'500':
|
||||
$ref: '#/responses/500'
|
||||
/replication/executions:
|
||||
get:
|
||||
summary: List replication executions
|
||||
@ -4407,6 +4553,78 @@ definitions:
|
||||
cve_id:
|
||||
type: string
|
||||
description: The ID of the CVE, such as "CVE-2019-10164"
|
||||
ReplicationPolicy:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The policy ID.
|
||||
name:
|
||||
type: string
|
||||
description: The policy name.
|
||||
description:
|
||||
type: string
|
||||
description: The description of the policy.
|
||||
src_registry:
|
||||
description: The source registry.
|
||||
$ref: '#/definitions/Registry'
|
||||
dest_registry:
|
||||
description: The destination registry.
|
||||
$ref: '#/definitions/Registry'
|
||||
dest_namespace:
|
||||
type: string
|
||||
description: The destination namespace.
|
||||
trigger:
|
||||
$ref: '#/definitions/ReplicationTrigger'
|
||||
filters:
|
||||
type: array
|
||||
description: The replication policy filter array.
|
||||
items:
|
||||
$ref: '#/definitions/ReplicationFilter'
|
||||
replicate_deletion:
|
||||
type: boolean
|
||||
description: Whether to replicate the deletion operation.
|
||||
deletion:
|
||||
type: boolean
|
||||
description: Deprecated, use "replicate_deletion" instead. Whether to replicate the deletion operation.
|
||||
override:
|
||||
type: boolean
|
||||
description: Whether to override the resources on the destination registry.
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether the policy is enabled or not.
|
||||
creation_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The create time of the policy.
|
||||
update_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The update time of the policy.
|
||||
ReplicationTrigger:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 'The replication policy trigger type. The valid values are manual, event_based and scheduled.'
|
||||
trigger_settings:
|
||||
$ref: '#/definitions/ReplicationTriggerSettings'
|
||||
ReplicationTriggerSettings:
|
||||
type: object
|
||||
properties:
|
||||
cron:
|
||||
type: string
|
||||
description: The cron string for scheduled trigger
|
||||
ReplicationFilter:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 'The replication policy filter type.'
|
||||
value:
|
||||
type: object
|
||||
description: 'The value of replication policy filter.'
|
||||
RegistryCredential:
|
||||
type: object
|
||||
properties:
|
||||
@ -4426,6 +4644,7 @@ definitions:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The registry ID.
|
||||
x-omitempty: false
|
||||
url:
|
||||
type: string
|
||||
description: The registry URL string.
|
||||
@ -4448,9 +4667,11 @@ definitions:
|
||||
description: Health status of the registry.
|
||||
creation_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The create time of the policy.
|
||||
update_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The update time of the policy.
|
||||
ResourceList:
|
||||
type: object
|
||||
|
@ -85,7 +85,7 @@ func constructReplicationPayload(event *event.ReplicationEvent) (*model.Payload,
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rpPolicy, err := rep.PolicyCtl.Get(execution.PolicyID)
|
||||
rpPolicy, err := replication.Ctl.GetPolicy(ctx, execution.PolicyID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get replication policy %d: error: %v", execution.PolicyID, err)
|
||||
return nil, nil, err
|
||||
|
@ -22,10 +22,11 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
rep "github.com/goharbor/harbor/src/controller/replication"
|
||||
repctl "github.com/goharbor/harbor/src/controller/replication"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
reppkg "github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
|
||||
@ -38,9 +39,6 @@ import (
|
||||
type fakedNotificationPolicyMgr struct {
|
||||
}
|
||||
|
||||
type fakedReplicationPolicyMgr struct {
|
||||
}
|
||||
|
||||
type fakedReplicationRegistryMgr struct {
|
||||
}
|
||||
|
||||
@ -87,44 +85,6 @@ func (f *fakedNotificationPolicyMgr) GetRelatedPolices(int64, string) ([]*models
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create new policy
|
||||
func (f *fakedReplicationPolicyMgr) Create(*model.Policy) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// List the policies, returns the total count, policy list and error
|
||||
func (f *fakedReplicationPolicyMgr) List(...*model.PolicyQuery) (int64, []*model.Policy, error) {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Get policy with specified ID
|
||||
func (f *fakedReplicationPolicyMgr) Get(int64) (*model.Policy, error) {
|
||||
return &model.Policy{
|
||||
ID: 1,
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get policy by the name
|
||||
func (f *fakedReplicationPolicyMgr) GetByName(string) (*model.Policy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Update the specified policy
|
||||
func (f *fakedReplicationPolicyMgr) Update(policy *model.Policy) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the specified policy
|
||||
func (f *fakedReplicationPolicyMgr) Remove(int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add new registry
|
||||
func (f *fakedReplicationRegistryMgr) Add(*model.Registry) (int64, error) {
|
||||
return 0, nil
|
||||
@ -171,27 +131,25 @@ func TestReplicationHandler_Handle(t *testing.T) {
|
||||
config.Init()
|
||||
|
||||
PolicyMgr := notification.PolicyMgr
|
||||
rpPolicy := replication.PolicyCtl
|
||||
rpRegistry := replication.RegistryMgr
|
||||
prj := project.Ctl
|
||||
repCtl := rep.Ctl
|
||||
repCtl := repctl.Ctl
|
||||
|
||||
defer func() {
|
||||
notification.PolicyMgr = PolicyMgr
|
||||
replication.PolicyCtl = rpPolicy
|
||||
replication.RegistryMgr = rpRegistry
|
||||
project.Ctl = prj
|
||||
rep.Ctl = repCtl
|
||||
repctl.Ctl = repCtl
|
||||
}()
|
||||
notification.PolicyMgr = &fakedNotificationPolicyMgr{}
|
||||
replication.PolicyCtl = &fakedReplicationPolicyMgr{}
|
||||
replication.RegistryMgr = &fakedReplicationRegistryMgr{}
|
||||
projectCtl := &projecttesting.Controller{}
|
||||
project.Ctl = projectCtl
|
||||
mockRepCtl := &replicationtesting.Controller{}
|
||||
rep.Ctl = mockRepCtl
|
||||
mockRepCtl.On("GetTask", mock.Anything, mock.Anything).Return(&rep.Task{}, nil)
|
||||
mockRepCtl.On("GetExecution", mock.Anything, mock.Anything).Return(&rep.Execution{}, nil)
|
||||
repctl.Ctl = mockRepCtl
|
||||
mockRepCtl.On("GetPolicy", mock.Anything, mock.Anything).Return(&reppkg.Policy{ID: 1}, nil)
|
||||
mockRepCtl.On("GetTask", mock.Anything, mock.Anything).Return(&repctl.Task{}, nil)
|
||||
mockRepCtl.On("GetExecution", mock.Anything, mock.Anything).Return(&repctl.Execution{}, nil)
|
||||
|
||||
mock.OnAnything(projectCtl, "GetByName").Return(&models.Project{ProjectID: 1}, nil)
|
||||
|
||||
|
@ -26,6 +26,9 @@ import (
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/reg"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
@ -35,10 +38,25 @@ func init() {
|
||||
task.SetExecutionSweeperCount(job.Replication, 50)
|
||||
}
|
||||
|
||||
// Ctl is a global replication controller instance
|
||||
var Ctl = NewController()
|
||||
|
||||
// Controller defines the operations related with replication
|
||||
type Controller interface {
|
||||
// PolicyCount returns the total count of policies according to the query
|
||||
PolicyCount(ctx context.Context, query *q.Query) (count int64, err error)
|
||||
// ListPolicies lists the policies according to the query
|
||||
ListPolicies(ctx context.Context, query *q.Query) (policies []*replication.Policy, err error)
|
||||
// GetPolicy gets the specific policy
|
||||
GetPolicy(ctx context.Context, id int64) (policy *replication.Policy, err error)
|
||||
// CreatePolicy creates a policy
|
||||
CreatePolicy(ctx context.Context, policy *replication.Policy) (id int64, err error)
|
||||
// UpdatePolicy updates the specific policy
|
||||
UpdatePolicy(ctx context.Context, policy *replication.Policy, props ...string) (err error)
|
||||
// DeletePolicy deletes the specific policy
|
||||
DeletePolicy(ctx context.Context, id int64) (err error)
|
||||
// Start the replication according to the policy
|
||||
Start(ctx context.Context, policy *model.Policy, resource *model.Resource, trigger string) (executionID int64, err error)
|
||||
Start(ctx context.Context, policy *replication.Policy, resource *model.Resource, trigger string) (executionID int64, err error)
|
||||
// Stop the replication specified by the execution ID
|
||||
Stop(ctx context.Context, executionID int64) (err error)
|
||||
// ExecutionCount returns the total count of executions according to the query
|
||||
@ -57,17 +75,14 @@ type Controller interface {
|
||||
GetTaskLog(ctx context.Context, taskID int64) (log []byte, err error)
|
||||
}
|
||||
|
||||
var (
|
||||
// Ctl is a global replication controller instance
|
||||
Ctl = NewController()
|
||||
_ Controller = &controller{}
|
||||
)
|
||||
|
||||
// NewController creates a new instance of the replication controller
|
||||
func NewController() Controller {
|
||||
return &controller{
|
||||
repMgr: replication.Mgr,
|
||||
execMgr: task.ExecMgr,
|
||||
taskMgr: task.Mgr,
|
||||
regMgr: reg.Mgr,
|
||||
scheduler: scheduler.Sched,
|
||||
flowCtl: flow.NewController(),
|
||||
ormCreator: orm.Crt,
|
||||
wp: lib.NewWorkerPool(1024),
|
||||
@ -75,14 +90,17 @@ func NewController() Controller {
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
repMgr replication.Manager
|
||||
execMgr task.ExecutionManager
|
||||
taskMgr task.Manager
|
||||
regMgr reg.Manager
|
||||
scheduler scheduler.Scheduler
|
||||
flowCtl flow.Controller
|
||||
ormCreator orm.Creator
|
||||
wp *lib.WorkerPool
|
||||
}
|
||||
|
||||
func (c *controller) Start(ctx context.Context, policy *model.Policy, resource *model.Resource, trigger string) (int64, error) {
|
||||
func (c *controller) Start(ctx context.Context, policy *replication.Policy, resource *model.Resource, trigger string) (int64, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
if !policy.Enabled {
|
||||
return 0, errors.New(nil).WithCode(errors.PreconditionCode).
|
@ -22,11 +22,14 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/goharbor/harbor/src/pkg/task/dao"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/testing/lib/orm"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
testingreg "github.com/goharbor/harbor/src/testing/pkg/reg"
|
||||
testingrep "github.com/goharbor/harbor/src/testing/pkg/replication"
|
||||
testingscheduler "github.com/goharbor/harbor/src/testing/pkg/scheduler"
|
||||
testingTask "github.com/goharbor/harbor/src/testing/pkg/task"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -34,18 +37,27 @@ import (
|
||||
type replicationTestSuite struct {
|
||||
suite.Suite
|
||||
ctl *controller
|
||||
repMgr *testingrep.Manager
|
||||
regMgr *testingreg.Manager
|
||||
execMgr *testingTask.ExecutionManager
|
||||
taskMgr *testingTask.Manager
|
||||
scheduler *testingscheduler.Scheduler
|
||||
flowCtl *flowController
|
||||
ormCreator *orm.Creator
|
||||
}
|
||||
|
||||
func (r *replicationTestSuite) SetupSuite() {
|
||||
func (r *replicationTestSuite) SetupTest() {
|
||||
r.repMgr = &testingrep.Manager{}
|
||||
r.regMgr = &testingreg.Manager{}
|
||||
r.execMgr = &testingTask.ExecutionManager{}
|
||||
r.taskMgr = &testingTask.Manager{}
|
||||
r.scheduler = &testingscheduler.Scheduler{}
|
||||
r.flowCtl = &flowController{}
|
||||
r.ormCreator = &orm.Creator{}
|
||||
r.ctl = &controller{
|
||||
repMgr: r.repMgr,
|
||||
regMgr: r.regMgr,
|
||||
scheduler: r.scheduler,
|
||||
execMgr: r.execMgr,
|
||||
taskMgr: r.taskMgr,
|
||||
flowCtl: r.flowCtl,
|
||||
@ -56,7 +68,7 @@ func (r *replicationTestSuite) SetupSuite() {
|
||||
|
||||
func (r *replicationTestSuite) TestStart() {
|
||||
// policy is disabled
|
||||
id, err := r.ctl.Start(context.Background(), &model.Policy{Enabled: false}, nil, task.ExecutionTriggerManual)
|
||||
id, err := r.ctl.Start(context.Background(), &replication.Policy{Enabled: false}, nil, task.ExecutionTriggerManual)
|
||||
r.Require().NotNil(err)
|
||||
|
||||
// got error when running the replication flow
|
||||
@ -66,7 +78,7 @@ func (r *replicationTestSuite) TestStart() {
|
||||
r.execMgr.On("MarkError", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
r.flowCtl.On("Start", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("error"))
|
||||
r.ormCreator.On("Create").Return(nil)
|
||||
id, err = r.ctl.Start(context.Background(), &model.Policy{Enabled: true}, nil, task.ExecutionTriggerManual)
|
||||
id, err = r.ctl.Start(context.Background(), &replication.Policy{Enabled: true}, nil, task.ExecutionTriggerManual)
|
||||
r.Require().Nil(err)
|
||||
r.Equal(int64(1), id)
|
||||
time.Sleep(1 * time.Second) // wait the functions called in the goroutine
|
||||
@ -75,14 +87,14 @@ func (r *replicationTestSuite) TestStart() {
|
||||
r.ormCreator.AssertExpectations(r.T())
|
||||
|
||||
// reset the mocks
|
||||
r.SetupSuite()
|
||||
r.SetupTest()
|
||||
|
||||
// got no error when running the replication flow
|
||||
r.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
r.execMgr.On("Get", mock.Anything, mock.Anything).Return(&task.Execution{}, nil)
|
||||
r.flowCtl.On("Start", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
r.ormCreator.On("Create").Return(nil)
|
||||
id, err = r.ctl.Start(context.Background(), &model.Policy{Enabled: true}, nil, task.ExecutionTriggerManual)
|
||||
id, err = r.ctl.Start(context.Background(), &replication.Policy{Enabled: true}, nil, task.ExecutionTriggerManual)
|
||||
r.Require().Nil(err)
|
||||
r.Equal(int64(1), id)
|
||||
time.Sleep(1 * time.Second) // wait the functions called in the goroutine
|
||||
@ -213,6 +225,11 @@ func (r *replicationTestSuite) TestGetTask() {
|
||||
}
|
||||
|
||||
func (r *replicationTestSuite) TestGetTaskLog() {
|
||||
r.taskMgr.On("List", mock.Anything, mock.Anything).Return([]*task.Task{
|
||||
{
|
||||
ID: 1,
|
||||
},
|
||||
}, nil)
|
||||
r.taskMgr.On("GetLog", mock.Anything, mock.Anything).Return([]byte{'a'}, nil)
|
||||
data, err := r.ctl.GetTaskLog(nil, 1)
|
||||
r.Require().Nil(err)
|
@ -16,6 +16,7 @@ package flow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
@ -27,7 +28,7 @@ type Flow interface {
|
||||
|
||||
// Controller controls the replication flow
|
||||
type Controller interface {
|
||||
Start(ctx context.Context, executionID int64, policy *model.Policy, resource *model.Resource) (err error)
|
||||
Start(ctx context.Context, executionID int64, policy *replication.Policy, resource *model.Resource) (err error)
|
||||
}
|
||||
|
||||
// NewController returns an instance of the default flow controller
|
||||
@ -37,7 +38,7 @@ func NewController() Controller {
|
||||
|
||||
type controller struct{}
|
||||
|
||||
func (c *controller) Start(ctx context.Context, executionID int64, policy *model.Policy, resource *model.Resource) error {
|
||||
func (c *controller) Start(ctx context.Context, executionID int64, policy *replication.Policy, resource *model.Resource) error {
|
||||
// deletion flow
|
||||
if resource != nil && resource.Deleted {
|
||||
return NewDeletionFlow(executionID, policy, resource).Run(ctx)
|
||||
|
@ -17,6 +17,7 @@ package flow
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
@ -27,7 +28,7 @@ import (
|
||||
type copyFlow struct {
|
||||
executionID int64
|
||||
resources []*model.Resource
|
||||
policy *model.Policy
|
||||
policy *replication.Policy
|
||||
executionMgr task.ExecutionManager
|
||||
taskMgr task.Manager
|
||||
}
|
||||
@ -35,7 +36,7 @@ type copyFlow struct {
|
||||
// NewCopyFlow returns an instance of the copy flow which replicates the resources from
|
||||
// the source registry to the destination registry. If the parameter "resources" isn't provided,
|
||||
// will fetch the resources first
|
||||
func NewCopyFlow(executionID int64, policy *model.Policy, resources ...*model.Resource) Flow {
|
||||
func NewCopyFlow(executionID int64, policy *replication.Policy, resources ...*model.Resource) Flow {
|
||||
return ©Flow{
|
||||
executionMgr: task.ExecMgr,
|
||||
taskMgr: task.Mgr,
|
||||
|
@ -13,6 +13,7 @@ package flow
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/replication/adapter"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
@ -60,7 +61,7 @@ func (c *copyFlowTestSuite) TestRun() {
|
||||
|
||||
taskMgr := &testingTask.Manager{}
|
||||
taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
policy := &model.Policy{
|
||||
policy := &replication.Policy{
|
||||
SrcRegistry: &model.Registry{
|
||||
Type: "TEST_FOR_COPY_FLOW",
|
||||
},
|
||||
|
@ -17,6 +17,7 @@ package flow
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
@ -25,7 +26,7 @@ import (
|
||||
|
||||
type deletionFlow struct {
|
||||
executionID int64
|
||||
policy *model.Policy
|
||||
policy *replication.Policy
|
||||
executionMgr task.ExecutionManager
|
||||
taskMgr task.Manager
|
||||
resources []*model.Resource
|
||||
@ -33,7 +34,7 @@ type deletionFlow struct {
|
||||
|
||||
// NewDeletionFlow returns an instance of the delete flow which deletes the resources
|
||||
// on the destination registry
|
||||
func NewDeletionFlow(executionID int64, policy *model.Policy, resources ...*model.Resource) Flow {
|
||||
func NewDeletionFlow(executionID int64, policy *replication.Policy, resources ...*model.Resource) Flow {
|
||||
return &deletionFlow{
|
||||
executionMgr: task.ExecMgr,
|
||||
taskMgr: task.Mgr,
|
||||
|
@ -16,6 +16,7 @@ package flow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
@ -32,7 +33,7 @@ func (d *deletionFlowTestSuite) TestRun() {
|
||||
taskMgr := &task.Manager{}
|
||||
taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
|
||||
policy := &model.Policy{
|
||||
policy := &replication.Policy{
|
||||
SrcRegistry: &model.Registry{
|
||||
Type: model.RegistryTypeHarbor,
|
||||
},
|
||||
|
@ -16,6 +16,7 @@ package flow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||
@ -24,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// get/create the source registry, destination registry, source adapter and destination adapter
|
||||
func initialize(policy *model.Policy) (adp.Adapter, adp.Adapter, error) {
|
||||
func initialize(policy *replication.Policy) (adp.Adapter, adp.Adapter, error) {
|
||||
var srcAdapter, dstAdapter adp.Adapter
|
||||
var err error
|
||||
|
||||
@ -52,7 +53,7 @@ func initialize(policy *model.Policy) (adp.Adapter, adp.Adapter, error) {
|
||||
}
|
||||
|
||||
// fetch resources from the source registry
|
||||
func fetchResources(adapter adp.Adapter, policy *model.Policy) ([]*model.Resource, error) {
|
||||
func fetchResources(adapter adp.Adapter, policy *replication.Policy) ([]*model.Resource, error) {
|
||||
var resTypes []model.ResourceType
|
||||
for _, filter := range policy.Filters {
|
||||
if filter.Type == model.FilterTypeResource {
|
||||
@ -111,7 +112,7 @@ func fetchResources(adapter adp.Adapter, policy *model.Policy) ([]*model.Resourc
|
||||
|
||||
// assemble the source resources by filling the registry information
|
||||
func assembleSourceResources(resources []*model.Resource,
|
||||
policy *model.Policy) []*model.Resource {
|
||||
policy *replication.Policy) []*model.Resource {
|
||||
for _, resource := range resources {
|
||||
resource.Registry = policy.SrcRegistry
|
||||
}
|
||||
@ -121,7 +122,7 @@ func assembleSourceResources(resources []*model.Resource,
|
||||
|
||||
// assemble the destination resources by filling the metadata, registry and override properties
|
||||
func assembleDestinationResources(resources []*model.Resource,
|
||||
policy *model.Policy) []*model.Resource {
|
||||
policy *replication.Policy) []*model.Resource {
|
||||
var result []*model.Resource
|
||||
for _, resource := range resources {
|
||||
res := &model.Resource{
|
||||
|
@ -15,6 +15,7 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/adapter"
|
||||
@ -35,7 +36,7 @@ func (s *stageTestSuite) TestInitialize() {
|
||||
factory.On("AdapterPattern").Return(nil)
|
||||
adapter.RegisterFactory(model.RegistryTypeHarbor, factory)
|
||||
|
||||
policy := &model.Policy{
|
||||
policy := &replication.Policy{
|
||||
SrcRegistry: &model.Registry{
|
||||
Type: model.RegistryTypeHarbor,
|
||||
},
|
||||
@ -60,7 +61,7 @@ func (s *stageTestSuite) TestFetchResources() {
|
||||
{},
|
||||
{},
|
||||
}, nil)
|
||||
policy := &model.Policy{}
|
||||
policy := &replication.Policy{}
|
||||
resources, err := fetchResources(adapter, policy)
|
||||
s.Require().Nil(err)
|
||||
s.Len(resources, 2)
|
||||
@ -80,7 +81,7 @@ func (s *stageTestSuite) TestAssembleSourceResources() {
|
||||
Override: false,
|
||||
},
|
||||
}
|
||||
policy := &model.Policy{
|
||||
policy := &replication.Policy{
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
@ -103,7 +104,7 @@ func (s *stageTestSuite) TestAssembleDestinationResources() {
|
||||
Override: false,
|
||||
},
|
||||
}
|
||||
policy := &model.Policy{
|
||||
policy := &replication.Policy{
|
||||
DestRegistry: &model.Registry{},
|
||||
DestNamespace: "test",
|
||||
Override: true,
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/goharbor/harbor/src/replication/model"
|
||||
|
||||
replication "github.com/goharbor/harbor/src/pkg/replication"
|
||||
)
|
||||
|
||||
// flowController is an autogenerated mock type for the Controller type
|
||||
@ -16,11 +18,11 @@ type flowController struct {
|
||||
}
|
||||
|
||||
// Start provides a mock function with given fields: ctx, executionID, policy, resource
|
||||
func (_m *flowController) Start(ctx context.Context, executionID int64, policy *model.Policy, resource *model.Resource) error {
|
||||
func (_m *flowController) Start(ctx context.Context, executionID int64, policy *replication.Policy, resource *model.Resource) error {
|
||||
ret := _m.Called(ctx, executionID, policy, resource)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *model.Policy, *model.Resource) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *replication.Policy, *model.Resource) error); ok {
|
||||
r0 = rf(ctx, executionID, policy, resource)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
|
188
src/controller/replication/policy.go
Normal file
188
src/controller/replication/policy.go
Normal file
@ -0,0 +1,188 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
commonthttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/goharbor/harbor/src/replication/config"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
|
||||
const callbackFuncName = "REPLICATION_CALLBACK"
|
||||
|
||||
func init() {
|
||||
callbackFunc := func(ctx context.Context, param string) error {
|
||||
policyID, err := strconv.ParseInt(param, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policy, err := Ctl.GetPolicy(ctx, policyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = Ctl.Start(ctx, policy, nil, task.ExecutionTriggerSchedule)
|
||||
return err
|
||||
}
|
||||
err := scheduler.RegisterCallbackFunc(callbackFuncName, callbackFunc)
|
||||
if err != nil {
|
||||
log.Errorf("failed to register the callback function for replication: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) PolicyCount(ctx context.Context, query *q.Query) (int64, error) {
|
||||
return c.repMgr.Count(ctx, query)
|
||||
}
|
||||
|
||||
func (c *controller) ListPolicies(ctx context.Context, query *q.Query) ([]*replication.Policy, error) {
|
||||
policies, err := c.repMgr.List(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, policy := range policies {
|
||||
if err := c.populateRegistry(ctx, policy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func (c *controller) populateRegistry(ctx context.Context, policy *replication.Policy) error {
|
||||
if policy.SrcRegistry != nil && policy.SrcRegistry.ID > 0 {
|
||||
registry, err := c.regMgr.Get(ctx, policy.SrcRegistry.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policy.SrcRegistry = registry
|
||||
policy.DestRegistry = GetLocalRegistry()
|
||||
return nil
|
||||
}
|
||||
|
||||
registry, err := c.regMgr.Get(ctx, policy.DestRegistry.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policy.DestRegistry = registry
|
||||
policy.SrcRegistry = GetLocalRegistry()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) GetPolicy(ctx context.Context, id int64) (*replication.Policy, error) {
|
||||
policy, err := c.repMgr.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.populateRegistry(ctx, policy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (c *controller) CreatePolicy(ctx context.Context, policy *replication.Policy) (int64, error) {
|
||||
if err := c.validatePolicy(ctx, policy); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// create policy
|
||||
id, err := c.repMgr.Create(ctx, policy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// create schedule if needed
|
||||
if policy.IsScheduledTrigger() {
|
||||
if _, err = c.scheduler.Schedule(ctx, job.Replication, id, "", policy.Trigger.Settings.Cron,
|
||||
callbackFuncName, policy.ID, map[string]interface{}{}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (c *controller) UpdatePolicy(ctx context.Context, policy *replication.Policy, props ...string) error {
|
||||
if err := c.validatePolicy(ctx, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
// delete the schedule
|
||||
if err := c.scheduler.UnScheduleByVendor(ctx, job.Replication, policy.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
// update the policy
|
||||
if err := c.repMgr.Update(ctx, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
// create schedule if needed
|
||||
if policy.IsScheduledTrigger() {
|
||||
if _, err := c.scheduler.Schedule(ctx, job.Replication, policy.ID, "", policy.Trigger.Settings.Cron,
|
||||
callbackFuncName, policy.ID, map[string]interface{}{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) validatePolicy(ctx context.Context, policy *replication.Policy) error {
|
||||
if err := policy.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if policy.SrcRegistry != nil {
|
||||
if _, err := c.regMgr.Get(ctx, policy.SrcRegistry.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if policy.DestRegistry != nil {
|
||||
if _, err := c.regMgr.Get(ctx, policy.DestRegistry.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) DeletePolicy(ctx context.Context, id int64) error {
|
||||
// delete the executions
|
||||
if err := c.execMgr.DeleteByVendor(ctx, job.Replication, id); err != nil {
|
||||
return err
|
||||
}
|
||||
// delete the schedule
|
||||
if err := c.scheduler.UnScheduleByVendor(ctx, job.Replication, id); err != nil {
|
||||
return err
|
||||
}
|
||||
// delete the policy
|
||||
return c.repMgr.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// GetLocalRegistry returns the info of the local Harbor registry
|
||||
// TODO move it into the registry package
|
||||
func GetLocalRegistry() *model.Registry {
|
||||
return &model.Registry{
|
||||
Type: model.RegistryTypeHarbor,
|
||||
Name: "Local",
|
||||
URL: config.Config.CoreURL,
|
||||
TokenServiceURL: config.Config.TokenServiceURL,
|
||||
Status: "healthy",
|
||||
Credential: &model.Credential{
|
||||
Type: model.CredentialTypeSecret,
|
||||
// use secret to do the auth for the local Harbor
|
||||
AccessSecret: config.Config.JobserviceSecret,
|
||||
},
|
||||
Insecure: !commonthttp.InternalTLSEnabled(),
|
||||
}
|
||||
}
|
133
src/controller/replication/policy_test.go
Normal file
133
src/controller/replication/policy_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/replication/config"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
)
|
||||
|
||||
func (r *replicationTestSuite) TestPolicyCount() {
|
||||
mock.OnAnything(r.repMgr, "Count").Return(int64(1), nil)
|
||||
count, err := r.ctl.PolicyCount(nil, nil)
|
||||
r.Require().Nil(err)
|
||||
r.Equal(int64(1), count)
|
||||
r.repMgr.AssertExpectations(r.T())
|
||||
}
|
||||
|
||||
func (r *replicationTestSuite) TestListPolicies() {
|
||||
mock.OnAnything(r.repMgr, "List").Return([]*replication.Policy{
|
||||
{
|
||||
ID: 1,
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
mock.OnAnything(r.regMgr, "Get").Return(&model.Registry{
|
||||
ID: 1,
|
||||
}, nil)
|
||||
config.Config = &config.Configuration{}
|
||||
policies, err := r.ctl.ListPolicies(nil, nil)
|
||||
r.Require().Nil(err)
|
||||
r.Require().Len(policies, 1)
|
||||
r.Equal(int64(1), policies[0].ID)
|
||||
r.repMgr.AssertExpectations(r.T())
|
||||
r.regMgr.AssertExpectations(r.T())
|
||||
}
|
||||
|
||||
func (r *replicationTestSuite) TestGetPolicy() {
|
||||
mock.OnAnything(r.repMgr, "Get").Return(&replication.Policy{
|
||||
ID: 1,
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
}, nil)
|
||||
mock.OnAnything(r.regMgr, "Get").Return(&model.Registry{
|
||||
ID: 1,
|
||||
}, nil)
|
||||
config.Config = &config.Configuration{}
|
||||
policy, err := r.ctl.GetPolicy(nil, 1)
|
||||
r.Require().Nil(err)
|
||||
r.Equal(int64(1), policy.ID)
|
||||
r.repMgr.AssertExpectations(r.T())
|
||||
r.regMgr.AssertExpectations(r.T())
|
||||
}
|
||||
|
||||
func (r *replicationTestSuite) TestCreatePolicy() {
|
||||
mock.OnAnything(r.repMgr, "Create").Return(int64(1), nil)
|
||||
mock.OnAnything(r.regMgr, "Get").Return(&model.Registry{
|
||||
ID: 1,
|
||||
}, nil)
|
||||
mock.OnAnything(r.scheduler, "Schedule").Return(int64(1), nil)
|
||||
id, err := r.ctl.CreatePolicy(nil, &replication.Policy{
|
||||
Name: "rule",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "0 * * * * *",
|
||||
},
|
||||
},
|
||||
Enabled: true,
|
||||
})
|
||||
r.Require().Nil(err)
|
||||
r.Equal(int64(1), id)
|
||||
r.repMgr.AssertExpectations(r.T())
|
||||
r.regMgr.AssertExpectations(r.T())
|
||||
r.scheduler.AssertExpectations(r.T())
|
||||
}
|
||||
|
||||
func (r *replicationTestSuite) TestUpdatePolicy() {
|
||||
mock.OnAnything(r.regMgr, "Get").Return(&model.Registry{
|
||||
ID: 1,
|
||||
}, nil)
|
||||
mock.OnAnything(r.scheduler, "UnScheduleByVendor").Return(nil)
|
||||
mock.OnAnything(r.scheduler, "Schedule").Return(int64(1), nil)
|
||||
mock.OnAnything(r.repMgr, "Update").Return(nil)
|
||||
err := r.ctl.UpdatePolicy(nil, &replication.Policy{
|
||||
ID: 1,
|
||||
Name: "rule",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "0 * * * * *",
|
||||
},
|
||||
},
|
||||
Enabled: true,
|
||||
})
|
||||
r.Require().Nil(err)
|
||||
r.repMgr.AssertExpectations(r.T())
|
||||
r.regMgr.AssertExpectations(r.T())
|
||||
r.scheduler.AssertExpectations(r.T())
|
||||
}
|
||||
|
||||
func (r *replicationTestSuite) TestDeletePolicy() {
|
||||
mock.OnAnything(r.execMgr, "DeleteByVendor").Return(nil)
|
||||
mock.OnAnything(r.scheduler, "UnScheduleByVendor").Return(nil)
|
||||
mock.OnAnything(r.repMgr, "Delete").Return(nil)
|
||||
err := r.ctl.DeletePolicy(nil, 1)
|
||||
r.Require().Nil(err)
|
||||
r.repMgr.AssertExpectations(r.T())
|
||||
r.execMgr.AssertExpectations(r.T())
|
||||
r.scheduler.AssertExpectations(r.T())
|
||||
}
|
@ -120,9 +120,6 @@ func init() {
|
||||
|
||||
beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List")
|
||||
|
||||
beego.Router("/api/replication/policies", &ReplicationPolicyAPI{}, "get:List;post:Create")
|
||||
beego.Router("/api/replication/policies/:id([0-9]+)", &ReplicationPolicyAPI{}, "get:Get;put:Update;delete:Delete")
|
||||
|
||||
beego.Router("/api/projects/:pid([0-9]+)/webhook/policies", &NotificationPolicyAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/projects/:pid([0-9]+)/webhook/policies/:id([0-9]+)", &NotificationPolicyAPI{})
|
||||
beego.Router("/api/projects/:pid([0-9]+)/webhook/policies/test", &NotificationPolicyAPI{}, "post:Test")
|
||||
|
@ -11,23 +11,21 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
rep "github.com/goharbor/harbor/src/controller/replication"
|
||||
"github.com/goharbor/harbor/src/core/api/models"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/adapter"
|
||||
"github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/replication/policy"
|
||||
"github.com/goharbor/harbor/src/replication/registry"
|
||||
)
|
||||
|
||||
// RegistryAPI handles requests to /api/registries/{}. It manages registries integrated to Harbor.
|
||||
type RegistryAPI struct {
|
||||
BaseController
|
||||
manager registry.Manager
|
||||
policyCtl policy.Controller
|
||||
resource types.Resource
|
||||
manager registry.Manager
|
||||
resource types.Resource
|
||||
}
|
||||
|
||||
// Prepare validates the user
|
||||
@ -40,7 +38,6 @@ func (t *RegistryAPI) Prepare() {
|
||||
t.resource = system.NewNamespace().Resource(rbac.ResourceRegistry)
|
||||
|
||||
t.manager = replication.RegistryMgr
|
||||
t.policyCtl = replication.PolicyCtl
|
||||
}
|
||||
|
||||
// Ping checks health status of a registry
|
||||
@ -368,11 +365,11 @@ func (t *RegistryAPI) Delete() {
|
||||
}
|
||||
|
||||
// Check whether there are replication policies that use this registry as source registry.
|
||||
total, _, err := t.policyCtl.List([]*model.PolicyQuery{
|
||||
{
|
||||
SrcRegistry: id,
|
||||
total, err := rep.Ctl.PolicyCount(orm.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"SrcRegistryID": id,
|
||||
},
|
||||
}...)
|
||||
})
|
||||
if err != nil {
|
||||
t.SendInternalServerError(fmt.Errorf("List replication policies with source registry %d error: %v", id, err))
|
||||
return
|
||||
@ -385,11 +382,11 @@ func (t *RegistryAPI) Delete() {
|
||||
}
|
||||
|
||||
// Check whether there are replication policies that use this registry as destination registry.
|
||||
total, _, err = t.policyCtl.List([]*model.PolicyQuery{
|
||||
{
|
||||
DestRegistry: id,
|
||||
total, err = rep.Ctl.PolicyCount(orm.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"DestRegistryID": id,
|
||||
},
|
||||
}...)
|
||||
})
|
||||
if err != nil {
|
||||
t.SendInternalServerError(fmt.Errorf("List replication policies with destination registry %d error: %v", id, err))
|
||||
return
|
||||
@ -434,7 +431,7 @@ func (t *RegistryAPI) GetInfo() {
|
||||
}
|
||||
var registry *model.Registry
|
||||
if id == 0 {
|
||||
registry = event.GetLocalRegistry()
|
||||
registry = rep.GetLocalRegistry()
|
||||
} else {
|
||||
registry, err = t.manager.Get(id)
|
||||
if err != nil {
|
||||
|
@ -1,289 +0,0 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/system"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
replica "github.com/goharbor/harbor/src/controller/replication"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/replication/registry"
|
||||
)
|
||||
|
||||
// TODO rename the file to "replication.go"
|
||||
|
||||
// ReplicationPolicyAPI handles the replication policy requests
|
||||
type ReplicationPolicyAPI struct {
|
||||
BaseController
|
||||
resource types.Resource
|
||||
}
|
||||
|
||||
// Prepare ...
|
||||
func (r *ReplicationPolicyAPI) Prepare() {
|
||||
r.BaseController.Prepare()
|
||||
if !r.SecurityCtx.IsAuthenticated() {
|
||||
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||
return
|
||||
}
|
||||
r.resource = system.NewNamespace().Resource(rbac.ResourceReplicationPolicy)
|
||||
}
|
||||
|
||||
// List the replication policies
|
||||
func (r *ReplicationPolicyAPI) List() {
|
||||
if !r.SecurityCtx.Can(r.Context(), rbac.ActionList, r.resource) {
|
||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
page, size, err := r.GetPaginationParams()
|
||||
if err != nil {
|
||||
r.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
// TODO: support more query
|
||||
query := &model.PolicyQuery{
|
||||
Name: r.GetString("name"),
|
||||
Page: page,
|
||||
Size: size,
|
||||
}
|
||||
|
||||
total, policies, err := replication.PolicyCtl.List(query)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to list policies: %v", err))
|
||||
return
|
||||
}
|
||||
for _, policy := range policies {
|
||||
if err = populateRegistries(replication.RegistryMgr, policy); err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", policy.ID, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
r.SetPaginationHeader(total, query.Page, query.Size)
|
||||
r.WriteJSONData(policies)
|
||||
}
|
||||
|
||||
// Create the replication policy
|
||||
func (r *ReplicationPolicyAPI) Create() {
|
||||
if !r.SecurityCtx.Can(r.Context(), rbac.ActionCreate, r.resource) {
|
||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
policy := &model.Policy{}
|
||||
isValid, err := r.DecodeJSONReqAndValidate(policy)
|
||||
if !isValid {
|
||||
r.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !r.validateName(policy) {
|
||||
return
|
||||
}
|
||||
if !r.validateRegistry(policy) {
|
||||
return
|
||||
}
|
||||
|
||||
policy.Creator = r.SecurityCtx.GetUsername()
|
||||
id, err := replication.PolicyCtl.Create(policy)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to create the policy: %v", err))
|
||||
return
|
||||
}
|
||||
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
||||
}
|
||||
|
||||
// make sure the policy name doesn't exist
|
||||
func (r *ReplicationPolicyAPI) validateName(policy *model.Policy) bool {
|
||||
p, err := replication.PolicyCtl.GetByName(policy.Name)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to get policy %s: %v", policy.Name, err))
|
||||
return false
|
||||
}
|
||||
if p != nil {
|
||||
r.SendConflictError(fmt.Errorf("policy %s already exists", policy.Name))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// make sure the registry referred exists
|
||||
func (r *ReplicationPolicyAPI) validateRegistry(policy *model.Policy) bool {
|
||||
var registryID int64
|
||||
if policy.SrcRegistry != nil && policy.SrcRegistry.ID > 0 {
|
||||
registryID = policy.SrcRegistry.ID
|
||||
} else {
|
||||
registryID = policy.DestRegistry.ID
|
||||
}
|
||||
registry, err := replication.RegistryMgr.Get(registryID)
|
||||
if err != nil {
|
||||
r.SendConflictError(fmt.Errorf("failed to get registry %d: %v", registryID, err))
|
||||
return false
|
||||
}
|
||||
if registry == nil {
|
||||
r.SendBadRequestError(fmt.Errorf("registry %d not found", registryID))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the specified replication policy
|
||||
func (r *ReplicationPolicyAPI) Get() {
|
||||
if !r.SecurityCtx.Can(r.Context(), rbac.ActionRead, r.resource) {
|
||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if id <= 0 || err != nil {
|
||||
r.SendBadRequestError(errors.New("invalid policy ID"))
|
||||
return
|
||||
}
|
||||
|
||||
policy, err := replication.PolicyCtl.Get(id)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
if policy == nil {
|
||||
r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
||||
return
|
||||
}
|
||||
if err = populateRegistries(replication.RegistryMgr, policy); err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to populate registries for policy %d: %v", policy.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.WriteJSONData(policy)
|
||||
}
|
||||
|
||||
// Update the replication policy
|
||||
func (r *ReplicationPolicyAPI) Update() {
|
||||
if !r.SecurityCtx.Can(r.Context(), rbac.ActionUpdate, r.resource) {
|
||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if id <= 0 || err != nil {
|
||||
r.SendBadRequestError(errors.New("invalid policy ID"))
|
||||
return
|
||||
}
|
||||
|
||||
originalPolicy, err := replication.PolicyCtl.Get(id)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
if originalPolicy == nil {
|
||||
r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
policy := &model.Policy{}
|
||||
isValid, err := r.DecodeJSONReqAndValidate(policy)
|
||||
if !isValid {
|
||||
r.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if policy.Name != originalPolicy.Name &&
|
||||
!r.validateName(policy) {
|
||||
return
|
||||
}
|
||||
|
||||
if !r.validateRegistry(policy) {
|
||||
return
|
||||
}
|
||||
|
||||
policy.ID = id
|
||||
if err := replication.PolicyCtl.Update(policy); err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to update the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the replication policy
|
||||
func (r *ReplicationPolicyAPI) Delete() {
|
||||
if !r.SecurityCtx.Can(r.Context(), rbac.ActionDelete, r.resource) {
|
||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if id <= 0 || err != nil {
|
||||
r.SendBadRequestError(errors.New("invalid policy ID"))
|
||||
return
|
||||
}
|
||||
|
||||
policy, err := replication.PolicyCtl.Get(id)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to get the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
if policy == nil {
|
||||
r.SendNotFoundError(fmt.Errorf("policy %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := orm.Context()
|
||||
executions, err := replica.Ctl.ListExecutions(ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"PolicyID": id,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
r.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
for _, execution := range executions {
|
||||
if execution.Status != job.RunningStatus.String() {
|
||||
continue
|
||||
}
|
||||
r.SendPreconditionFailedError(fmt.Errorf("the policy %d has running executions, can not be deleted", id))
|
||||
return
|
||||
}
|
||||
for _, execution := range executions {
|
||||
if err = task.ExecMgr.Delete(ctx, execution.ID); err != nil {
|
||||
r.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := replication.PolicyCtl.Remove(id); err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to delete the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ignore the credential for the registries
|
||||
func populateRegistries(registryMgr registry.Manager, policy *model.Policy) error {
|
||||
if err := event.PopulateRegistries(registryMgr, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
if policy.SrcRegistry != nil {
|
||||
hideAccessSecret(policy.SrcRegistry.Credential)
|
||||
}
|
||||
if policy.DestRegistry != nil {
|
||||
hideAccessSecret(policy.DestRegistry.Credential)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,438 +0,0 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
|
||||
// TODO rename the file to "replication.go"
|
||||
|
||||
type fakedRegistryManager struct{}
|
||||
|
||||
func (f *fakedRegistryManager) Add(*model.Registry) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) List(query *q.Query) (int64, []*model.Registry, error) {
|
||||
return 0, nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Get(id int64) (*model.Registry, error) {
|
||||
if id == 1 {
|
||||
return &model.Registry{
|
||||
Type: "faked_registry",
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) GetByName(string) (*model.Registry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Update(*model.Registry, ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Remove(int64) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedRegistryManager) HealthCheck() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakedPolicyManager struct{}
|
||||
|
||||
func (f *fakedPolicyManager) Create(*model.Policy) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakedPolicyManager) List(...*model.PolicyQuery) (int64, []*model.Policy, error) {
|
||||
return 0, nil, nil
|
||||
}
|
||||
func (f *fakedPolicyManager) Get(id int64) (*model.Policy, error) {
|
||||
if id == 1 {
|
||||
return &model.Policy{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if id == 2 {
|
||||
return &model.Policy{
|
||||
ID: 2,
|
||||
Enabled: false,
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedPolicyManager) GetByName(name string) (*model.Policy, error) {
|
||||
if name == "duplicate_name" {
|
||||
return &model.Policy{
|
||||
Name: "duplicate_name",
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedPolicyManager) Update(*model.Policy) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedPolicyManager) Remove(int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPIList(t *testing.T) {
|
||||
policyMgr := replication.PolicyCtl
|
||||
defer func() {
|
||||
replication.PolicyCtl = policyMgr
|
||||
}()
|
||||
replication.PolicyCtl = &fakedPolicyManager{}
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/replication/policies",
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/replication/policies",
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPICreate(t *testing.T) {
|
||||
policyMgr := replication.PolicyCtl
|
||||
registryMgr := replication.RegistryMgr
|
||||
defer func() {
|
||||
replication.PolicyCtl = policyMgr
|
||||
replication.RegistryMgr = registryMgr
|
||||
}()
|
||||
replication.PolicyCtl = &fakedPolicyManager{}
|
||||
replication.RegistryMgr = &fakedRegistryManager{}
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 400 empty policy name
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400 empty registry
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 409, duplicate policy name
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "duplicate_name",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusConflict,
|
||||
},
|
||||
// 400, registry not found
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 201
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusCreated,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPIGet(t *testing.T) {
|
||||
policyMgr := replication.PolicyCtl
|
||||
registryMgr := replication.RegistryMgr
|
||||
defer func() {
|
||||
replication.PolicyCtl = policyMgr
|
||||
replication.RegistryMgr = registryMgr
|
||||
}()
|
||||
replication.PolicyCtl = &fakedPolicyManager{}
|
||||
replication.RegistryMgr = &fakedRegistryManager{}
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/replication/policies/1",
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 404, policy not found
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/replication/policies/3",
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPIUpdate(t *testing.T) {
|
||||
policyMgr := replication.PolicyCtl
|
||||
registryMgr := replication.RegistryMgr
|
||||
defer func() {
|
||||
replication.PolicyCtl = policyMgr
|
||||
replication.RegistryMgr = registryMgr
|
||||
}()
|
||||
replication.PolicyCtl = &fakedPolicyManager{}
|
||||
replication.RegistryMgr = &fakedRegistryManager{}
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 404 policy not found
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/3",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{},
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 400 empty policy name
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 409, duplicate policy name
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "duplicate_name",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusConflict,
|
||||
},
|
||||
// 400, registry not found
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPIDelete(t *testing.T) {
|
||||
policyMgr := replication.PolicyCtl
|
||||
defer func() {
|
||||
replication.PolicyCtl = policyMgr
|
||||
}()
|
||||
replication.PolicyCtl = &fakedPolicyManager{}
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: "/api/replication/policies/1",
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 404, policy not found
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: "/api/replication/policies/3",
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
128
src/pkg/reg/dao/dao.go
Normal file
128
src/pkg/reg/dao/dao.go
Normal file
@ -0,0 +1,128 @@
|
||||
// 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 (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||
)
|
||||
|
||||
// DAO defines the DAO operations of registry
|
||||
type DAO interface {
|
||||
// Create the registry
|
||||
Create(ctx context.Context, registry *models.Registry) (id int64, err error)
|
||||
// Count returns the count of registries according to the query
|
||||
Count(ctx context.Context, query *q.Query) (count int64, err error)
|
||||
// List the registries according to the query
|
||||
List(ctx context.Context, query *q.Query) (registries []*models.Registry, err error)
|
||||
// Get the registry specified by ID
|
||||
Get(ctx context.Context, id int64) (registry *models.Registry, err error)
|
||||
// Update the specified registry
|
||||
Update(ctx context.Context, registry *models.Registry, props ...string) (err error)
|
||||
// Delete the registry specified by ID
|
||||
Delete(ctx context.Context, id int64) (err error)
|
||||
}
|
||||
|
||||
// NewDAO creates an instance of DAO
|
||||
func NewDAO() DAO {
|
||||
return &dao{}
|
||||
}
|
||||
|
||||
type dao struct{}
|
||||
|
||||
func (d *dao) Create(ctx context.Context, registry *models.Registry) (int64, error) {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := ormer.Insert(registry)
|
||||
if e := orm.AsConflictError(err, "registry %s already exists", registry.Name); e != nil {
|
||||
err = e
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
qs, err := orm.QuerySetterForCount(ctx, &models.Registry{}, query)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return qs.Count()
|
||||
}
|
||||
|
||||
func (d *dao) List(ctx context.Context, query *q.Query) ([]*models.Registry, error) {
|
||||
registries := []*models.Registry{}
|
||||
qs, err := orm.QuerySetter(ctx, &models.Registry{}, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = qs.All(®istries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
func (d *dao) Get(ctx context.Context, id int64) (*models.Registry, error) {
|
||||
registry := &models.Registry{
|
||||
ID: id,
|
||||
}
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ormer.Read(registry); err != nil {
|
||||
if e := orm.AsNotFoundError(err, "registry %d not found", id); e != nil {
|
||||
err = e
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
func (d *dao) Update(ctx context.Context, registry *models.Registry, props ...string) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := ormer.Update(registry, props...)
|
||||
if err != nil {
|
||||
if e := orm.AsConflictError(err, "registry %s already exists", registry.Name); e != nil {
|
||||
err = e
|
||||
}
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.NotFoundError(nil).WithMessage("registry %d not found", registry.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dao) Delete(ctx context.Context, id int64) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := ormer.Delete(&models.Registry{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.NotFoundError(nil).WithMessage("registry %d not found", id)
|
||||
}
|
||||
return nil
|
||||
}
|
162
src/pkg/reg/dao/dao_test.go
Normal file
162
src/pkg/reg/dao/dao_test.go
Normal file
@ -0,0 +1,162 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
beegoorm "github.com/astaxie/beego/orm"
|
||||
common_dao "github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type daoTestSuite struct {
|
||||
suite.Suite
|
||||
dao DAO
|
||||
ctx context.Context
|
||||
id int64
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) SetupSuite() {
|
||||
d.dao = NewDAO()
|
||||
common_dao.PrepareTestForPostgresSQL()
|
||||
d.ctx = orm.NewContext(nil, beegoorm.NewOrm())
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) SetupTest() {
|
||||
registry := &models.Registry{
|
||||
URL: "http://harbor.local",
|
||||
Name: "harbor",
|
||||
Type: "harbor",
|
||||
Insecure: false,
|
||||
Health: "health",
|
||||
}
|
||||
id, err := d.dao.Create(d.ctx, registry)
|
||||
d.Require().Nil(err)
|
||||
d.id = id
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TearDownTest() {
|
||||
err := d.dao.Delete(d.ctx, d.id)
|
||||
d.Require().Nil(err)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestCount() {
|
||||
// nil query
|
||||
total, err := d.dao.Count(d.ctx, nil)
|
||||
d.Require().Nil(err)
|
||||
d.True(total > 0)
|
||||
|
||||
// query by name
|
||||
total, err = d.dao.Count(d.ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"Name": "harbor",
|
||||
},
|
||||
})
|
||||
d.Require().Nil(err)
|
||||
d.Equal(int64(1), total)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestList() {
|
||||
// nil query
|
||||
registries, err := d.dao.List(d.ctx, nil)
|
||||
d.Require().Nil(err)
|
||||
found := false
|
||||
for _, registry := range registries {
|
||||
if registry.ID == d.id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
d.True(found)
|
||||
|
||||
// query by name
|
||||
registries, err = d.dao.List(d.ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"Name": "harbor",
|
||||
},
|
||||
})
|
||||
d.Require().Nil(err)
|
||||
d.Require().Equal(1, len(registries))
|
||||
d.Equal(d.id, registries[0].ID)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestGet() {
|
||||
// get the non-exist registry
|
||||
_, err := d.dao.Get(d.ctx, 10000)
|
||||
d.Require().NotNil(err)
|
||||
d.True(errors.IsErr(err, errors.NotFoundCode))
|
||||
|
||||
// get the exist registry
|
||||
registry, err := d.dao.Get(d.ctx, d.id)
|
||||
d.Require().Nil(err)
|
||||
d.Require().NotNil(registry)
|
||||
d.Equal(d.id, registry.ID)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestCreate() {
|
||||
// the happy pass case is covered in Setup
|
||||
|
||||
// conflict
|
||||
registry := &models.Registry{
|
||||
Name: "harbor",
|
||||
}
|
||||
_, err := d.dao.Create(d.ctx, registry)
|
||||
d.Require().NotNil(err)
|
||||
d.True(errors.IsErr(err, errors.ConflictCode))
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestDelete() {
|
||||
// the happy pass case is covered in TearDown
|
||||
|
||||
// not exist
|
||||
err := d.dao.Delete(d.ctx, 100021)
|
||||
d.Require().NotNil(err)
|
||||
var e *errors.Error
|
||||
d.Require().True(errors.As(err, &e))
|
||||
d.Equal(errors.NotFoundCode, e.Code)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestUpdate() {
|
||||
// pass
|
||||
err := d.dao.Update(d.ctx, &models.Registry{
|
||||
ID: d.id,
|
||||
Description: "description",
|
||||
}, "Description")
|
||||
d.Require().Nil(err)
|
||||
|
||||
registry, err := d.dao.Get(d.ctx, d.id)
|
||||
d.Require().Nil(err)
|
||||
d.Require().NotNil(registry)
|
||||
d.Equal("description", registry.Description)
|
||||
|
||||
// not exist
|
||||
err = d.dao.Update(d.ctx, &models.Registry{
|
||||
ID: 10000,
|
||||
})
|
||||
d.Require().NotNil(err)
|
||||
var e *errors.Error
|
||||
d.Require().True(errors.As(err, &e))
|
||||
d.Equal(errors.NotFoundCode, e.Code)
|
||||
}
|
||||
|
||||
func TestDaoTestSuite(t *testing.T) {
|
||||
suite.Run(t, &daoTestSuite{})
|
||||
}
|
104
src/pkg/reg/manager.go
Normal file
104
src/pkg/reg/manager.go
Normal file
@ -0,0 +1,104 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/dao"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
reg "github.com/goharbor/harbor/src/replication/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
// Mgr is the global registry manager instance
|
||||
Mgr = NewManager()
|
||||
)
|
||||
|
||||
// Manager defines the registry related operations
|
||||
type Manager interface {
|
||||
// Create the registry
|
||||
Create(ctx context.Context, registry *model.Registry) (id int64, err error)
|
||||
// Count returns the count of registries according to the query
|
||||
Count(ctx context.Context, query *q.Query) (count int64, err error)
|
||||
// List registries according to the query
|
||||
List(ctx context.Context, query *q.Query) (registries []*model.Registry, err error)
|
||||
// Get the registry specified by ID
|
||||
Get(ctx context.Context, id int64) (registry *model.Registry, err error)
|
||||
// Update the specified registry
|
||||
Update(ctx context.Context, registry *model.Registry, props ...string) (err error)
|
||||
// Delete the registry specified by ID
|
||||
Delete(ctx context.Context, id int64) (err error)
|
||||
}
|
||||
|
||||
// NewManager creates an instance of registry manager
|
||||
func NewManager() Manager {
|
||||
return &manager{
|
||||
dao: dao.NewDAO(),
|
||||
}
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
dao dao.DAO
|
||||
}
|
||||
|
||||
func (m *manager) Create(ctx context.Context, registry *model.Registry) (int64, error) {
|
||||
reg, err := reg.ToDaoModel(registry)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return m.dao.Create(ctx, reg)
|
||||
}
|
||||
|
||||
func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
return m.dao.Count(ctx, query)
|
||||
}
|
||||
|
||||
func (m *manager) List(ctx context.Context, query *q.Query) ([]*model.Registry, error) {
|
||||
registries, err := m.dao.List(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var regs []*model.Registry
|
||||
for _, registry := range registries {
|
||||
r, err := reg.FromDaoModel(registry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regs = append(regs, r)
|
||||
}
|
||||
return regs, nil
|
||||
}
|
||||
|
||||
func (m *manager) Get(ctx context.Context, id int64) (*model.Registry, error) {
|
||||
registry, err := m.dao.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reg.FromDaoModel(registry)
|
||||
}
|
||||
|
||||
func (m *manager) Update(ctx context.Context, registry *model.Registry, props ...string) error {
|
||||
reg, err := reg.ToDaoModel(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.dao.Update(ctx, reg, props...)
|
||||
}
|
||||
|
||||
func (m *manager) Delete(ctx context.Context, id int64) error {
|
||||
return m.dao.Delete(ctx, id)
|
||||
}
|
94
src/pkg/reg/manager_test.go
Normal file
94
src/pkg/reg/manager_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/reg/dao"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type managerTestSuite struct {
|
||||
suite.Suite
|
||||
mgr *manager
|
||||
dao *dao.DAO
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) SetupTest() {
|
||||
m.dao = &dao.DAO{}
|
||||
m.mgr = &manager{
|
||||
dao: m.dao,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestCount() {
|
||||
mock.OnAnything(m.dao, "Count").Return(int64(1), nil)
|
||||
n, err := m.mgr.Count(nil, nil)
|
||||
m.Require().Nil(err)
|
||||
m.Equal(int64(1), n)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestList() {
|
||||
mock.OnAnything(m.dao, "List").Return([]*models.Registry{
|
||||
{
|
||||
ID: 1,
|
||||
},
|
||||
}, nil)
|
||||
registries, err := m.mgr.List(nil, nil)
|
||||
m.Require().Nil(err)
|
||||
m.Require().Equal(1, len(registries))
|
||||
m.Equal(int64(1), registries[0].ID)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestGet() {
|
||||
mock.OnAnything(m.dao, "Get").Return(&models.Registry{
|
||||
ID: 1,
|
||||
}, nil)
|
||||
registry, err := m.mgr.Get(nil, 1)
|
||||
m.Require().Nil(err)
|
||||
m.Equal(int64(1), registry.ID)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestCreate() {
|
||||
mock.OnAnything(m.dao, "Create").Return(int64(1), nil)
|
||||
_, err := m.mgr.Create(nil, &model.Registry{})
|
||||
m.Require().Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestDelete() {
|
||||
mock.OnAnything(m.dao, "Delete").Return(nil)
|
||||
err := m.mgr.Delete(nil, 1)
|
||||
m.Require().Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestUpdate() {
|
||||
mock.OnAnything(m.dao, "Update").Return(nil)
|
||||
err := m.mgr.Update(nil, &model.Registry{})
|
||||
m.Require().Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func TestManager(t *testing.T) {
|
||||
suite.Run(t, &managerTestSuite{})
|
||||
}
|
127
src/pkg/replication/dao/dao.go
Normal file
127
src/pkg/replication/dao/dao.go
Normal file
@ -0,0 +1,127 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
)
|
||||
|
||||
// DAO defines the DAO operations of replication policy
|
||||
type DAO interface {
|
||||
// Count returns the count of replication policies according to the query
|
||||
Count(ctx context.Context, query *q.Query) (count int64, err error)
|
||||
// List the replication policies according to the query
|
||||
List(ctx context.Context, query *q.Query) (policies []*Policy, err error)
|
||||
// Get the replication policy specified by ID
|
||||
Get(ctx context.Context, id int64) (policy *Policy, err error)
|
||||
// Create the replication policy
|
||||
Create(ctx context.Context, policy *Policy) (id int64, err error)
|
||||
// Update the specified replication policy
|
||||
Update(ctx context.Context, policy *Policy, props ...string) (err error)
|
||||
// Delete the replication policy specified by ID
|
||||
Delete(ctx context.Context, id int64) (err error)
|
||||
}
|
||||
|
||||
// NewDAO creates an instance of DAO
|
||||
func NewDAO() DAO {
|
||||
return &dao{}
|
||||
}
|
||||
|
||||
type dao struct{}
|
||||
|
||||
func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
qs, err := orm.QuerySetterForCount(ctx, &Policy{}, query)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return qs.Count()
|
||||
}
|
||||
|
||||
func (d *dao) List(ctx context.Context, query *q.Query) ([]*Policy, error) {
|
||||
policies := []*Policy{}
|
||||
qs, err := orm.QuerySetter(ctx, &Policy{}, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = qs.All(&policies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func (d *dao) Get(ctx context.Context, id int64) (*Policy, error) {
|
||||
policy := &Policy{
|
||||
ID: id,
|
||||
}
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ormer.Read(policy); err != nil {
|
||||
if e := orm.AsNotFoundError(err, "replication policy %d not found", id); e != nil {
|
||||
err = e
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (d *dao) Create(ctx context.Context, policy *Policy) (int64, error) {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := ormer.Insert(policy)
|
||||
if e := orm.AsConflictError(err, "replication policy %s already exists", policy.Name); e != nil {
|
||||
err = e
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (d *dao) Update(ctx context.Context, policy *Policy, props ...string) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := ormer.Update(policy, props...)
|
||||
if e := orm.AsConflictError(err, "replication policy %s already exists", policy.Name); e != nil {
|
||||
err = e
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.NotFoundError(nil).WithMessage("replication policy %d not found", policy.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dao) Delete(ctx context.Context, id int64) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := ormer.Delete(&Policy{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.NotFoundError(nil).WithMessage("replication policy %d not found", id)
|
||||
}
|
||||
return nil
|
||||
}
|
157
src/pkg/replication/dao/dao_test.go
Normal file
157
src/pkg/replication/dao/dao_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
beegoorm "github.com/astaxie/beego/orm"
|
||||
common_dao "github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type daoTestSuite struct {
|
||||
suite.Suite
|
||||
dao DAO
|
||||
ctx context.Context
|
||||
id int64
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) SetupSuite() {
|
||||
d.dao = NewDAO()
|
||||
common_dao.PrepareTestForPostgresSQL()
|
||||
d.ctx = orm.NewContext(nil, beegoorm.NewOrm())
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) SetupTest() {
|
||||
registry := &Policy{
|
||||
Name: "test-rule",
|
||||
}
|
||||
id, err := d.dao.Create(d.ctx, registry)
|
||||
d.Require().Nil(err)
|
||||
d.id = id
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TearDownTest() {
|
||||
err := d.dao.Delete(d.ctx, d.id)
|
||||
d.Require().Nil(err)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestCount() {
|
||||
// nil query
|
||||
total, err := d.dao.Count(d.ctx, nil)
|
||||
d.Require().Nil(err)
|
||||
d.True(total > 0)
|
||||
|
||||
// query by name
|
||||
total, err = d.dao.Count(d.ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"Name": "test-rule",
|
||||
},
|
||||
})
|
||||
d.Require().Nil(err)
|
||||
d.Equal(int64(1), total)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestList() {
|
||||
// nil query
|
||||
policies, err := d.dao.List(d.ctx, nil)
|
||||
d.Require().Nil(err)
|
||||
found := false
|
||||
for _, policy := range policies {
|
||||
if policy.ID == d.id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
d.True(found)
|
||||
|
||||
// query by name
|
||||
policies, err = d.dao.List(d.ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"Name": "test-rule",
|
||||
},
|
||||
})
|
||||
d.Require().Nil(err)
|
||||
d.Require().Equal(1, len(policies))
|
||||
d.Equal(d.id, policies[0].ID)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestGet() {
|
||||
// get the non-exist policy
|
||||
_, err := d.dao.Get(d.ctx, 10000)
|
||||
d.Require().NotNil(err)
|
||||
d.True(errors.IsErr(err, errors.NotFoundCode))
|
||||
|
||||
// get the exist policy
|
||||
policy, err := d.dao.Get(d.ctx, d.id)
|
||||
d.Require().Nil(err)
|
||||
d.Require().NotNil(policy)
|
||||
d.Equal(d.id, policy.ID)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestCreate() {
|
||||
// the happy pass case is covered in Setup
|
||||
|
||||
// conflict
|
||||
policy := &Policy{
|
||||
Name: "test-rule",
|
||||
}
|
||||
_, err := d.dao.Create(d.ctx, policy)
|
||||
d.Require().NotNil(err)
|
||||
d.True(errors.IsErr(err, errors.ConflictCode))
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestDelete() {
|
||||
// the happy pass case is covered in TearDown
|
||||
|
||||
// not exist
|
||||
err := d.dao.Delete(d.ctx, 100021)
|
||||
d.Require().NotNil(err)
|
||||
var e *errors.Error
|
||||
d.Require().True(errors.As(err, &e))
|
||||
d.Equal(errors.NotFoundCode, e.Code)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestUpdate() {
|
||||
// pass
|
||||
err := d.dao.Update(d.ctx, &Policy{
|
||||
ID: d.id,
|
||||
Description: "description",
|
||||
}, "Description")
|
||||
d.Require().Nil(err)
|
||||
|
||||
policy, err := d.dao.Get(d.ctx, d.id)
|
||||
d.Require().Nil(err)
|
||||
d.Require().NotNil(policy)
|
||||
d.Equal("description", policy.Description)
|
||||
|
||||
// not exist
|
||||
err = d.dao.Update(d.ctx, &Policy{
|
||||
ID: 10000,
|
||||
})
|
||||
d.Require().NotNil(err)
|
||||
var e *errors.Error
|
||||
d.Require().True(errors.As(err, &e))
|
||||
d.Equal(errors.NotFoundCode, e.Code)
|
||||
}
|
||||
|
||||
func TestDaoTestSuite(t *testing.T) {
|
||||
suite.Run(t, &daoTestSuite{})
|
||||
}
|
48
src/pkg/replication/dao/model.go
Normal file
48
src/pkg/replication/dao/model.go
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Policy))
|
||||
}
|
||||
|
||||
// Policy is the model for replication policy
|
||||
type Policy struct {
|
||||
ID int64 `orm:"pk;auto;column(id)"`
|
||||
Name string `orm:"column(name)"`
|
||||
Description string `orm:"column(description)"`
|
||||
Creator string `orm:"column(creator)"`
|
||||
SrcRegistryID int64 `orm:"column(src_registry_id)"`
|
||||
DestRegistryID int64 `orm:"column(dest_registry_id)"`
|
||||
DestNamespace string `orm:"column(dest_namespace)"`
|
||||
Override bool `orm:"column(override)"`
|
||||
Enabled bool `orm:"column(enabled)"`
|
||||
Trigger string `orm:"column(trigger)"`
|
||||
Filters string `orm:"column(filters)"`
|
||||
ReplicateDeletion bool `orm:"column(replicate_deletion)"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" sort:"default:desc"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now"`
|
||||
}
|
||||
|
||||
// TableName set table name for ORM
|
||||
func (p *Policy) TableName() string {
|
||||
return "replication_policy"
|
||||
}
|
106
src/pkg/replication/manager.go
Normal file
106
src/pkg/replication/manager.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/replication/dao"
|
||||
)
|
||||
|
||||
var (
|
||||
// Mgr is the global replication policy manager instance
|
||||
Mgr = NewManager()
|
||||
)
|
||||
|
||||
// Manager defines the replication policy related operations
|
||||
type Manager interface {
|
||||
// Count returns the count of replication policies according to the query
|
||||
Count(ctx context.Context, query *q.Query) (count int64, err error)
|
||||
// List replication policies according to the query
|
||||
List(ctx context.Context, query *q.Query) (policies []*Policy, err error)
|
||||
// Get the replication policy specified by ID
|
||||
Get(ctx context.Context, id int64) (policy *Policy, err error)
|
||||
// Create the replication policy
|
||||
Create(ctx context.Context, policy *Policy) (id int64, err error)
|
||||
// Update the specified replication policy
|
||||
Update(ctx context.Context, policy *Policy, props ...string) (err error)
|
||||
// Delete the replication policy specified by ID
|
||||
Delete(ctx context.Context, id int64) (err error)
|
||||
}
|
||||
|
||||
// NewManager creates an instance of replication policy manager
|
||||
func NewManager() Manager {
|
||||
return &manager{
|
||||
dao: dao.NewDAO(),
|
||||
}
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
dao dao.DAO
|
||||
}
|
||||
|
||||
func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
return m.dao.Count(ctx, query)
|
||||
}
|
||||
|
||||
func (m *manager) List(ctx context.Context, query *q.Query) ([]*Policy, error) {
|
||||
policies, err := m.dao.List(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []*Policy
|
||||
for _, policy := range policies {
|
||||
p := &Policy{}
|
||||
if err = p.From(policy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *manager) Get(ctx context.Context, id int64) (*Policy, error) {
|
||||
policy, err := m.dao.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := &Policy{}
|
||||
if err = p.From(policy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (m *manager) Create(ctx context.Context, policy *Policy) (int64, error) {
|
||||
p, err := policy.To()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return m.dao.Create(ctx, p)
|
||||
}
|
||||
|
||||
func (m *manager) Update(ctx context.Context, policy *Policy, props ...string) error {
|
||||
p, err := policy.To()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.dao.Update(ctx, p, props...)
|
||||
}
|
||||
|
||||
func (m *manager) Delete(ctx context.Context, id int64) error {
|
||||
return m.dao.Delete(ctx, id)
|
||||
}
|
93
src/pkg/replication/manager_test.go
Normal file
93
src/pkg/replication/manager_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/replication/dao"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
testingdao "github.com/goharbor/harbor/src/testing/pkg/replication/dao"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type managerTestSuite struct {
|
||||
suite.Suite
|
||||
mgr *manager
|
||||
dao *testingdao.DAO
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) SetupTest() {
|
||||
m.dao = &testingdao.DAO{}
|
||||
m.mgr = &manager{
|
||||
dao: m.dao,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestCount() {
|
||||
mock.OnAnything(m.dao, "Count").Return(int64(1), nil)
|
||||
n, err := m.mgr.Count(nil, nil)
|
||||
m.Require().Nil(err)
|
||||
m.Equal(int64(1), n)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestList() {
|
||||
mock.OnAnything(m.dao, "List").Return([]*dao.Policy{
|
||||
{
|
||||
ID: 1,
|
||||
},
|
||||
}, nil)
|
||||
policies, err := m.mgr.List(nil, nil)
|
||||
m.Require().Nil(err)
|
||||
m.Require().Equal(1, len(policies))
|
||||
m.Equal(int64(1), policies[0].ID)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestGet() {
|
||||
mock.OnAnything(m.dao, "Get").Return(&dao.Policy{
|
||||
ID: 1,
|
||||
}, nil)
|
||||
policy, err := m.mgr.Get(nil, 1)
|
||||
m.Require().Nil(err)
|
||||
m.Equal(int64(1), policy.ID)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestCreate() {
|
||||
mock.OnAnything(m.dao, "Create").Return(int64(1), nil)
|
||||
_, err := m.mgr.Create(nil, &Policy{})
|
||||
m.Require().Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestDelete() {
|
||||
mock.OnAnything(m.dao, "Delete").Return(nil)
|
||||
err := m.mgr.Delete(nil, 1)
|
||||
m.Require().Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestUpdate() {
|
||||
mock.OnAnything(m.dao, "Update").Return(nil)
|
||||
err := m.mgr.Update(nil, &Policy{})
|
||||
m.Require().Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func TestManager(t *testing.T) {
|
||||
suite.Run(t, &managerTestSuite{})
|
||||
}
|
350
src/pkg/replication/model.go
Normal file
350
src/pkg/replication/model.go
Normal file
@ -0,0 +1,350 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/robfig/cron"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/replication/dao"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
|
||||
// Policy defines the structure of a replication policy
|
||||
type Policy struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Creator string `json:"creator"`
|
||||
SrcRegistry *model.Registry `json:"src_registry"`
|
||||
DestRegistry *model.Registry `json:"dest_registry"`
|
||||
DestNamespace string `json:"dest_namespace"`
|
||||
Filters []*model.Filter `json:"filters"`
|
||||
Trigger *model.Trigger `json:"trigger"`
|
||||
ReplicateDeletion bool `json:"deletion"`
|
||||
Override bool `json:"override"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// IsScheduledTrigger returns true when the policy is scheduled trigger and enabled
|
||||
func (p *Policy) IsScheduledTrigger() bool {
|
||||
if !p.Enabled {
|
||||
return false
|
||||
}
|
||||
if p.Trigger == nil {
|
||||
return false
|
||||
}
|
||||
return p.Trigger.Type == model.TriggerTypeScheduled
|
||||
}
|
||||
|
||||
// Validate the policy
|
||||
func (p *Policy) Validate() error {
|
||||
if len(p.Name) == 0 {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).WithMessage("empty name")
|
||||
}
|
||||
var srcRegistryID, dstRegistryID int64
|
||||
if p.SrcRegistry != nil {
|
||||
srcRegistryID = p.SrcRegistry.ID
|
||||
}
|
||||
if p.DestRegistry != nil {
|
||||
dstRegistryID = p.DestRegistry.ID
|
||||
}
|
||||
|
||||
// one of the source registry and destination registry must be Harbor itself
|
||||
if srcRegistryID != 0 && dstRegistryID != 0 ||
|
||||
srcRegistryID == 0 && dstRegistryID == 0 {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("either src_registry or dest_registry should be empty and the other one shouldn't be empty")
|
||||
}
|
||||
|
||||
// valid the filters
|
||||
for _, filter := range p.Filters {
|
||||
switch filter.Type {
|
||||
case model.FilterTypeResource, model.FilterTypeName, model.FilterTypeTag:
|
||||
value, ok := filter.Value.(string)
|
||||
if !ok {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("the type of filter value isn't string")
|
||||
}
|
||||
if filter.Type == model.FilterTypeResource {
|
||||
rt := model.ResourceType(value)
|
||||
if !(rt == model.ResourceTypeArtifact || rt == model.ResourceTypeImage || rt == model.ResourceTypeChart) {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("invalid resource filter: %s", value)
|
||||
}
|
||||
}
|
||||
case model.FilterTypeLabel:
|
||||
labels, ok := filter.Value.([]interface{})
|
||||
if !ok {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("the type of label filter value isn't string slice")
|
||||
}
|
||||
for _, label := range labels {
|
||||
_, ok := label.(string)
|
||||
if !ok {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("the type of label filter value isn't string slice")
|
||||
}
|
||||
}
|
||||
default:
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("invalid filter type")
|
||||
}
|
||||
}
|
||||
|
||||
// valid trigger
|
||||
if p.Trigger != nil {
|
||||
switch p.Trigger.Type {
|
||||
case model.TriggerTypeManual, model.TriggerTypeEventBased:
|
||||
case model.TriggerTypeScheduled:
|
||||
if p.Trigger.Settings == nil || len(p.Trigger.Settings.Cron) == 0 {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("the cron string cannot be empty when the trigger type is %s", model.TriggerTypeScheduled)
|
||||
}
|
||||
if _, err := cron.Parse(p.Trigger.Settings.Cron); err != nil {
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("invalid cron string for scheduled trigger: %s", p.Trigger.Settings.Cron)
|
||||
}
|
||||
default:
|
||||
return errors.New(nil).WithCode(errors.BadRequestCode).
|
||||
WithMessage("invalid trigger type")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// From converts the DAO model into the Policy
|
||||
func (p *Policy) From(policy *dao.Policy) error {
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
p.ID = policy.ID
|
||||
p.Name = policy.Name
|
||||
p.Description = policy.Description
|
||||
p.Creator = policy.Creator
|
||||
p.DestNamespace = policy.DestNamespace
|
||||
p.ReplicateDeletion = policy.ReplicateDeletion
|
||||
p.Override = policy.Override
|
||||
p.Enabled = policy.Enabled
|
||||
p.CreationTime = policy.CreationTime
|
||||
p.UpdateTime = policy.UpdateTime
|
||||
|
||||
if policy.SrcRegistryID > 0 {
|
||||
p.SrcRegistry = &model.Registry{
|
||||
ID: policy.SrcRegistryID,
|
||||
}
|
||||
}
|
||||
if policy.DestRegistryID > 0 {
|
||||
p.DestRegistry = &model.Registry{
|
||||
ID: policy.DestRegistryID,
|
||||
}
|
||||
}
|
||||
|
||||
// parse Filters
|
||||
filters, err := parseFilters(policy.Filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Filters = filters
|
||||
|
||||
// parse Trigger
|
||||
trigger, err := parseTrigger(policy.Trigger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Trigger = trigger
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// To converts to DAO model
|
||||
func (p *Policy) To() (*dao.Policy, error) {
|
||||
policy := &dao.Policy{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
Creator: p.Creator,
|
||||
DestNamespace: p.DestNamespace,
|
||||
Override: p.Override,
|
||||
Enabled: p.Enabled,
|
||||
ReplicateDeletion: p.ReplicateDeletion,
|
||||
CreationTime: p.CreationTime,
|
||||
UpdateTime: p.UpdateTime,
|
||||
}
|
||||
if p.SrcRegistry != nil {
|
||||
policy.SrcRegistryID = p.SrcRegistry.ID
|
||||
}
|
||||
if p.DestRegistry != nil {
|
||||
policy.DestRegistryID = p.DestRegistry.ID
|
||||
}
|
||||
|
||||
if p.Trigger != nil {
|
||||
trigger, err := json.Marshal(p.Trigger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy.Trigger = string(trigger)
|
||||
}
|
||||
|
||||
if len(p.Filters) > 0 {
|
||||
filters, err := json.Marshal(p.Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy.Filters = string(filters)
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
type filter struct {
|
||||
Type model.FilterType `json:"type"`
|
||||
Value interface{} `json:"value"`
|
||||
Kind string `json:"kind"`
|
||||
Pattern string `json:"pattern"`
|
||||
}
|
||||
|
||||
type trigger struct {
|
||||
Type model.TriggerType `json:"type"`
|
||||
Settings *model.TriggerSettings `json:"trigger_settings"`
|
||||
Kind string `json:"kind"`
|
||||
ScheduleParam *scheduleParam `json:"schedule_param"`
|
||||
}
|
||||
|
||||
type scheduleParam struct {
|
||||
Type string `json:"type"`
|
||||
Weekday int8 `json:"weekday"`
|
||||
Offtime int64 `json:"offtime"`
|
||||
}
|
||||
|
||||
func parseFilters(str string) ([]*model.Filter, error) {
|
||||
if len(str) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
items := []*filter{}
|
||||
if err := json.Unmarshal([]byte(str), &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filters := []*model.Filter{}
|
||||
for _, item := range items {
|
||||
filter := &model.Filter{
|
||||
Type: item.Type,
|
||||
Value: item.Value,
|
||||
}
|
||||
// keep backwards compatibility
|
||||
if len(filter.Type) == 0 {
|
||||
if filter.Value == nil {
|
||||
filter.Value = item.Pattern
|
||||
}
|
||||
switch item.Kind {
|
||||
case "repository":
|
||||
// a name filter "project_name/**" must exist after running upgrade
|
||||
// if there is any repository filter, merge it into the name filter
|
||||
repository, ok := filter.Value.(string)
|
||||
if ok && len(repository) > 0 {
|
||||
for _, item := range items {
|
||||
if item.Type == model.FilterTypeName {
|
||||
name, ok := item.Value.(string)
|
||||
if ok && len(name) > 0 {
|
||||
item.Value = strings.Replace(name, "**", repository, 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
case "tag":
|
||||
filter.Type = model.FilterTypeTag
|
||||
case "label":
|
||||
// drop all legend label filters
|
||||
continue
|
||||
default:
|
||||
log.Warningf("unknown filter type: %s", filter.Type)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// convert the type of value from string to model.ResourceType if the filter
|
||||
// is a resource type filter
|
||||
if filter.Type == model.FilterTypeResource {
|
||||
filter.Value = (model.ResourceType)(filter.Value.(string))
|
||||
}
|
||||
if filter.Type == model.FilterTypeLabel {
|
||||
labels := []string{}
|
||||
for _, label := range filter.Value.([]interface{}) {
|
||||
labels = append(labels, label.(string))
|
||||
}
|
||||
filter.Value = labels
|
||||
}
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func parseTrigger(str string) (*model.Trigger, error) {
|
||||
if len(str) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
item := &trigger{}
|
||||
if err := json.Unmarshal([]byte(str), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trigger := &model.Trigger{
|
||||
Type: item.Type,
|
||||
Settings: item.Settings,
|
||||
}
|
||||
// keep backwards compatibility
|
||||
if len(trigger.Type) == 0 {
|
||||
switch item.Kind {
|
||||
case "Manual":
|
||||
trigger.Type = model.TriggerTypeManual
|
||||
case "Immediate":
|
||||
trigger.Type = model.TriggerTypeEventBased
|
||||
case "Scheduled":
|
||||
trigger.Type = model.TriggerTypeScheduled
|
||||
trigger.Settings = &model.TriggerSettings{
|
||||
Cron: parseScheduleParamToCron(item.ScheduleParam),
|
||||
}
|
||||
default:
|
||||
log.Warningf("unknown trigger type: %s", item.Kind)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
||||
func parseScheduleParamToCron(param *scheduleParam) string {
|
||||
if param == nil {
|
||||
return ""
|
||||
}
|
||||
offtime := param.Offtime
|
||||
offtime = offtime % (3600 * 24)
|
||||
hour := int(offtime / 3600)
|
||||
offtime = offtime % 3600
|
||||
minute := int(offtime / 60)
|
||||
second := int(offtime % 60)
|
||||
if param.Type == "Weekly" {
|
||||
return fmt.Sprintf("%d %d %d * * %d", second, minute, hour, param.Weekday%7)
|
||||
}
|
||||
return fmt.Sprintf("%d %d %d * * *", second, minute, hour)
|
||||
}
|
252
src/pkg/replication/model_test.go
Normal file
252
src/pkg/replication/model_test.go
Normal file
@ -0,0 +1,252 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package replication
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsScheduledTrigger(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// policy is disabled
|
||||
policy := &Policy{
|
||||
Enabled: false,
|
||||
}
|
||||
b := policy.IsScheduledTrigger()
|
||||
assert.False(b)
|
||||
|
||||
// no trigger
|
||||
policy = &Policy{
|
||||
Enabled: true,
|
||||
}
|
||||
b = policy.IsScheduledTrigger()
|
||||
assert.False(b)
|
||||
|
||||
// isn't scheduled trigger
|
||||
policy = &Policy{
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeEventBased,
|
||||
},
|
||||
Enabled: true,
|
||||
}
|
||||
b = policy.IsScheduledTrigger()
|
||||
assert.False(b)
|
||||
|
||||
// scheduled trigger
|
||||
policy = &Policy{
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
},
|
||||
Enabled: true,
|
||||
}
|
||||
b = policy.IsScheduledTrigger()
|
||||
assert.True(b)
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// empty name
|
||||
policy := &Policy{}
|
||||
err := policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// empty source registry and destination registry
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// source registry and destination registry both not empty
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 2,
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// invalid filter
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: "invalid_type",
|
||||
},
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// invalid filter
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeResource,
|
||||
Value: "invalid_resource_type",
|
||||
},
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// invalid filter
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeResource,
|
||||
Value: model.ResourceTypeImage,
|
||||
},
|
||||
{
|
||||
Type: model.FilterTypeTag,
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// invalid trigger
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library",
|
||||
},
|
||||
},
|
||||
Trigger: &model.Trigger{
|
||||
Type: "invalid_type",
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// invalid trigger
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library",
|
||||
},
|
||||
},
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// invalid cron
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeResource,
|
||||
Value: "image",
|
||||
},
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library/**",
|
||||
},
|
||||
},
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "* * *",
|
||||
},
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.True(errors.IsErr(err, errors.BadRequestCode))
|
||||
|
||||
// pass
|
||||
policy = &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeResource,
|
||||
Value: "image",
|
||||
},
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library/**",
|
||||
},
|
||||
},
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "* * * * * *",
|
||||
},
|
||||
},
|
||||
}
|
||||
err = policy.Validate()
|
||||
assert.Nil(err)
|
||||
}
|
@ -63,6 +63,9 @@ type ExecutionManager interface {
|
||||
StopAndWait(ctx context.Context, id int64, timeout time.Duration) (err error)
|
||||
// Delete the specified execution and its tasks
|
||||
Delete(ctx context.Context, id int64) (err error)
|
||||
// Delete all executions and tasks of the specific vendor. They can be deleted only when all the executions/tasks
|
||||
// of the vendor are in final status
|
||||
DeleteByVendor(ctx context.Context, vendorType string, vendorID int64) (err error)
|
||||
// Get the specified execution
|
||||
Get(ctx context.Context, id int64) (execution *Execution, err error)
|
||||
// List executions according to the query
|
||||
@ -334,6 +337,34 @@ func (e *executionManager) Delete(ctx context.Context, id int64) error {
|
||||
return e.executionDAO.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (e *executionManager) DeleteByVendor(ctx context.Context, vendorType string, vendorID int64) error {
|
||||
executions, err := e.executionDAO.List(ctx, &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"VendorType": vendorType,
|
||||
"VendorID": vendorID,
|
||||
}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// check the status
|
||||
for _, execution := range executions {
|
||||
if !job.Status(execution.Status).Final() {
|
||||
return errors.New(nil).WithCode(errors.PreconditionCode).
|
||||
WithMessage("contains executions that aren't in final status, stop the execution first")
|
||||
}
|
||||
}
|
||||
// delete the executions
|
||||
for _, execution := range executions {
|
||||
if err = e.Delete(ctx, execution.ID); err != nil {
|
||||
if errors.IsNotFoundErr(err) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *executionManager) Get(ctx context.Context, id int64) (*Execution, error) {
|
||||
execution, err := e.executionDAO.Get(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -6,6 +6,5 @@ import (
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(
|
||||
new(Registry),
|
||||
new(RepPolicy))
|
||||
new(Registry))
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// RepPolicy is the model for a ng replication policy.
|
||||
type RepPolicy struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Description string `orm:"column(description)" json:"description"`
|
||||
Creator string `orm:"column(creator)" json:"creator"`
|
||||
SrcRegistryID int64 `orm:"column(src_registry_id)" json:"src_registry_id"`
|
||||
DestRegistryID int64 `orm:"column(dest_registry_id)" json:"dest_registry_id"`
|
||||
DestNamespace string `orm:"column(dest_namespace)" json:"dest_namespace"`
|
||||
Override bool `orm:"column(override)" json:"override"`
|
||||
Enabled bool `orm:"column(enabled)" json:"enabled"`
|
||||
Trigger string `orm:"column(trigger)" json:"trigger"`
|
||||
Filters string `orm:"column(filters)" json:"filters"`
|
||||
ReplicateDeletion bool `orm:"column(replicate_deletion)" json:"replicate_deletion"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// TableName set table name for ORM.
|
||||
func (r *RepPolicy) TableName() string {
|
||||
return "replication_policy"
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
common_dao "github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
|
||||
// AddRepPolicy insert new policy to DB.
|
||||
func AddRepPolicy(policy *models.RepPolicy) (int64, error) {
|
||||
o := common_dao.GetOrmer()
|
||||
now := time.Now()
|
||||
policy.CreationTime = now
|
||||
policy.UpdateTime = now
|
||||
|
||||
return o.Insert(policy)
|
||||
}
|
||||
|
||||
// GetPolicies list polices with given query parameters.
|
||||
func GetPolicies(queries ...*model.PolicyQuery) (int64, []*models.RepPolicy, error) {
|
||||
var qs = common_dao.GetOrmer().QueryTable(new(models.RepPolicy))
|
||||
var policies []*models.RepPolicy
|
||||
|
||||
if len(queries) == 0 {
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
|
||||
_, err = qs.All(&policies)
|
||||
if err != nil {
|
||||
return total, nil, err
|
||||
}
|
||||
|
||||
return total, policies, nil
|
||||
}
|
||||
|
||||
query := queries[0]
|
||||
if len(query.Name) != 0 {
|
||||
qs = qs.Filter("Name__icontains", common_dao.Escape(query.Name))
|
||||
}
|
||||
if len(query.Namespace) != 0 {
|
||||
// TODO: Namespace filter not implemented yet
|
||||
}
|
||||
if query.SrcRegistry > 0 {
|
||||
qs = qs.Filter("SrcRegistryID__exact", query.SrcRegistry)
|
||||
}
|
||||
if query.DestRegistry > 0 {
|
||||
qs = qs.Filter("DestRegistryID__exact", query.DestRegistry)
|
||||
}
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
|
||||
if query.Page > 0 && query.Size > 0 {
|
||||
qs = qs.Limit(query.Size, (query.Page-1)*query.Size)
|
||||
}
|
||||
_, err = qs.OrderBy("-CreationTime").All(&policies)
|
||||
if err != nil {
|
||||
return total, nil, err
|
||||
}
|
||||
|
||||
return total, policies, nil
|
||||
}
|
||||
|
||||
// GetRepPolicy return special policy by id.
|
||||
func GetRepPolicy(id int64) (policy *models.RepPolicy, err error) {
|
||||
policy = new(models.RepPolicy)
|
||||
err = common_dao.GetOrmer().QueryTable(policy).
|
||||
Filter("id", id).One(policy)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetRepPolicyByName return special policy by name.
|
||||
func GetRepPolicyByName(name string) (policy *models.RepPolicy, err error) {
|
||||
policy = new(models.RepPolicy)
|
||||
err = common_dao.GetOrmer().QueryTable(policy).
|
||||
Filter("name", name).One(policy)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateRepPolicy update fields by props
|
||||
func UpdateRepPolicy(policy *models.RepPolicy, props ...string) (err error) {
|
||||
var o = common_dao.GetOrmer()
|
||||
|
||||
if policy != nil {
|
||||
_, err = o.Update(policy, props...)
|
||||
} else {
|
||||
err = errors.New("Nil policy")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteRepPolicy will hard delete database item
|
||||
func DeleteRepPolicy(id int64) error {
|
||||
o := common_dao.GetOrmer()
|
||||
|
||||
_, err := o.Delete(&models.RepPolicy{ID: id})
|
||||
return err
|
||||
}
|
@ -1,284 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/dao/models"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
testPolic1 = &models.RepPolicy{
|
||||
// ID: 999,
|
||||
Name: "Policy Test 1",
|
||||
Description: "Policy Description",
|
||||
Creator: "someone",
|
||||
SrcRegistryID: 123,
|
||||
DestRegistryID: 456,
|
||||
DestNamespace: "target_ns",
|
||||
ReplicateDeletion: true,
|
||||
Override: true,
|
||||
Enabled: true,
|
||||
Trigger: "{\"type\":\"\",\"trigger_settings\":null}",
|
||||
Filters: "[{\"type\":\"registry\",\"value\":\"abc\"}]",
|
||||
}
|
||||
|
||||
testPolic2 = &models.RepPolicy{
|
||||
// ID: 999,
|
||||
Name: "Policy Test 2",
|
||||
Description: "Policy Description",
|
||||
Creator: "someone",
|
||||
SrcRegistryID: 123,
|
||||
DestRegistryID: 456,
|
||||
DestNamespace: "target_ns",
|
||||
ReplicateDeletion: true,
|
||||
Override: true,
|
||||
Enabled: true,
|
||||
Trigger: "{\"type\":\"\",\"trigger_settings\":null}",
|
||||
Filters: "[{\"type\":\"registry\",\"value\":\"abc\"}]",
|
||||
}
|
||||
|
||||
testPolic3 = &models.RepPolicy{
|
||||
// ID: 999,
|
||||
Name: "Policy Test 3",
|
||||
Description: "Policy Description",
|
||||
Creator: "someone",
|
||||
SrcRegistryID: 123,
|
||||
DestRegistryID: 456,
|
||||
DestNamespace: "target_ns",
|
||||
ReplicateDeletion: true,
|
||||
Override: true,
|
||||
Enabled: true,
|
||||
Trigger: "{\"type\":\"\",\"trigger_settings\":null}",
|
||||
Filters: "[{\"type\":\"registry\",\"value\":\"abc\"}]",
|
||||
}
|
||||
)
|
||||
|
||||
func TestAddRepPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policy *models.RepPolicy
|
||||
want int64
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "AddRepPolicy 1", policy: testPolic1, want: 1},
|
||||
{name: "AddRepPolicy 2", policy: testPolic2, want: 2},
|
||||
{name: "AddRepPolicy 3", policy: testPolic3, want: 3},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := AddRepPolicy(tt.policy)
|
||||
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err, "wantErr: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPolicies(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
namespace string
|
||||
page int64
|
||||
pageSize int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantPolicies []*models.RepPolicy
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "GetTotalOfRepPolicies nil", args: args{name: "Test 0"}, wantPolicies: []*models.RepPolicy{}},
|
||||
{name: "GetTotalOfRepPolicies 1", args: args{name: "Test 1"}, wantPolicies: []*models.RepPolicy{testPolic1}},
|
||||
{name: "GetTotalOfRepPolicies 2", args: args{name: "Test", page: 1, pageSize: 2}, wantPolicies: []*models.RepPolicy{testPolic3, testPolic2}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, gotPolicies, err := GetPolicies([]*model.PolicyQuery{
|
||||
{
|
||||
Name: tt.args.name,
|
||||
Namespace: tt.args.namespace,
|
||||
Page: tt.args.page,
|
||||
Size: tt.args.pageSize,
|
||||
},
|
||||
}...)
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err, "wantErr: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err)
|
||||
for i, gotPolicy := range gotPolicies {
|
||||
assert.Equal(t, tt.wantPolicies[i].Name, gotPolicy.Name)
|
||||
assert.Equal(t, tt.wantPolicies[i].Description, gotPolicy.Description)
|
||||
assert.Equal(t, tt.wantPolicies[i].Creator, gotPolicy.Creator)
|
||||
assert.Equal(t, tt.wantPolicies[i].SrcRegistryID, gotPolicy.SrcRegistryID)
|
||||
assert.Equal(t, tt.wantPolicies[i].DestRegistryID, gotPolicy.DestRegistryID)
|
||||
assert.Equal(t, tt.wantPolicies[i].DestNamespace, gotPolicy.DestNamespace)
|
||||
assert.Equal(t, tt.wantPolicies[i].ReplicateDeletion, gotPolicy.ReplicateDeletion)
|
||||
assert.Equal(t, tt.wantPolicies[i].Override, gotPolicy.Override)
|
||||
assert.Equal(t, tt.wantPolicies[i].Enabled, gotPolicy.Enabled)
|
||||
assert.Equal(t, tt.wantPolicies[i].Trigger, gotPolicy.Trigger)
|
||||
assert.Equal(t, tt.wantPolicies[i].Filters, gotPolicy.Filters)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id int64
|
||||
wantPolicy *models.RepPolicy
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "GetRepPolicy 1", id: 1, wantPolicy: testPolic1},
|
||||
{name: "GetRepPolicy 2", id: 2, wantPolicy: testPolic2},
|
||||
{name: "GetRepPolicy 3", id: 3, wantPolicy: testPolic3},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotPolicy, err := GetRepPolicy(tt.id)
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err, "wantErr: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tt.wantPolicy.Name, gotPolicy.Name)
|
||||
assert.Equal(t, tt.wantPolicy.Description, gotPolicy.Description)
|
||||
assert.Equal(t, tt.wantPolicy.Creator, gotPolicy.Creator)
|
||||
assert.Equal(t, tt.wantPolicy.SrcRegistryID, gotPolicy.SrcRegistryID)
|
||||
assert.Equal(t, tt.wantPolicy.DestRegistryID, gotPolicy.DestRegistryID)
|
||||
assert.Equal(t, tt.wantPolicy.DestNamespace, gotPolicy.DestNamespace)
|
||||
assert.Equal(t, tt.wantPolicy.ReplicateDeletion, gotPolicy.ReplicateDeletion)
|
||||
assert.Equal(t, tt.wantPolicy.Override, gotPolicy.Override)
|
||||
assert.Equal(t, tt.wantPolicy.Enabled, gotPolicy.Enabled)
|
||||
assert.Equal(t, tt.wantPolicy.Trigger, gotPolicy.Trigger)
|
||||
assert.Equal(t, tt.wantPolicy.Filters, gotPolicy.Filters)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepPolicyByName(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantPolicy *models.RepPolicy
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "GetRepPolicyByName 1", args: args{name: testPolic1.Name}, wantPolicy: testPolic1},
|
||||
{name: "GetRepPolicyByName 2", args: args{name: testPolic2.Name}, wantPolicy: testPolic2},
|
||||
{name: "GetRepPolicyByName 3", args: args{name: testPolic3.Name}, wantPolicy: testPolic3},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotPolicy, err := GetRepPolicyByName(tt.args.name)
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err, "wantErr: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tt.wantPolicy.Name, gotPolicy.Name)
|
||||
assert.Equal(t, tt.wantPolicy.Description, gotPolicy.Description)
|
||||
assert.Equal(t, tt.wantPolicy.Creator, gotPolicy.Creator)
|
||||
assert.Equal(t, tt.wantPolicy.SrcRegistryID, gotPolicy.SrcRegistryID)
|
||||
assert.Equal(t, tt.wantPolicy.DestRegistryID, gotPolicy.DestRegistryID)
|
||||
assert.Equal(t, tt.wantPolicy.DestNamespace, gotPolicy.DestNamespace)
|
||||
assert.Equal(t, tt.wantPolicy.ReplicateDeletion, gotPolicy.ReplicateDeletion)
|
||||
assert.Equal(t, tt.wantPolicy.Override, gotPolicy.Override)
|
||||
assert.Equal(t, tt.wantPolicy.Enabled, gotPolicy.Enabled)
|
||||
assert.Equal(t, tt.wantPolicy.Trigger, gotPolicy.Trigger)
|
||||
assert.Equal(t, tt.wantPolicy.Filters, gotPolicy.Filters)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepPolicy(t *testing.T) {
|
||||
type args struct {
|
||||
policy *models.RepPolicy
|
||||
props []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "UpdateRepPolicy Want Error", args: args{policy: nil}, wantErr: true},
|
||||
{
|
||||
name: "UpdateRepPolicy 1",
|
||||
args: args{
|
||||
policy: &models.RepPolicy{ID: 1, Description: "Policy Description 1", Creator: "Someone 1"},
|
||||
props: []string{"description", "creator"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "UpdateRepPolicy 2",
|
||||
args: args{
|
||||
policy: &models.RepPolicy{ID: 2, Description: "Policy Description 2", Creator: "Someone 2"},
|
||||
props: []string{"description", "creator"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "UpdateRepPolicy 3",
|
||||
args: args{
|
||||
policy: &models.RepPolicy{ID: 3, Description: "Policy Description 3", Creator: "Someone 3"},
|
||||
props: []string{"description", "creator"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := UpdateRepPolicy(tt.args.policy, tt.args.props...)
|
||||
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err, "Error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err)
|
||||
gotPolicy, err := GetRepPolicy(tt.args.policy.ID)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, tt.args.policy.Description, gotPolicy.Description)
|
||||
assert.Equal(t, tt.args.policy.Creator, gotPolicy.Creator)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id int64
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "DeleteRepPolicy 1", id: 1, wantErr: false},
|
||||
{name: "DeleteRepPolicy 2", id: 2, wantErr: false},
|
||||
{name: "DeleteRepPolicy 3", id: 3, wantErr: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := DeleteRepPolicy(tt.id)
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err, "wantErr: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err)
|
||||
policy, err := GetRepPolicy(tt.id)
|
||||
require.Nil(t, err)
|
||||
assert.Nil(t, policy)
|
||||
})
|
||||
}
|
||||
}
|
@ -18,15 +18,13 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
commonthttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/controller/replication"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
rep "github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/goharbor/harbor/src/replication/config"
|
||||
"github.com/goharbor/harbor/src/replication/filter"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/replication/policy"
|
||||
"github.com/goharbor/harbor/src/replication/registry"
|
||||
)
|
||||
|
||||
@ -36,16 +34,14 @@ type Handler interface {
|
||||
}
|
||||
|
||||
// NewHandler ...
|
||||
func NewHandler(policyCtl policy.Controller, registryMgr registry.Manager) Handler {
|
||||
func NewHandler(registryMgr registry.Manager) Handler {
|
||||
return &handler{
|
||||
policyCtl: policyCtl,
|
||||
registryMgr: registryMgr,
|
||||
ctl: replication.Ctl,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
policyCtl policy.Controller
|
||||
registryMgr registry.Manager
|
||||
ctl replication.Controller
|
||||
}
|
||||
@ -56,7 +52,7 @@ func (h *handler) Handle(event *Event) error {
|
||||
len(event.Resource.Metadata.Artifacts) == 0 {
|
||||
return errors.New("invalid event")
|
||||
}
|
||||
var policies []*model.Policy
|
||||
var policies []*rep.Policy
|
||||
var err error
|
||||
switch event.Type {
|
||||
case EventTypeArtifactPush, EventTypeChartUpload, EventTypeTagDelete,
|
||||
@ -75,9 +71,6 @@ func (h *handler) Handle(event *Event) error {
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if err := PopulateRegistries(h.registryMgr, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := h.ctl.Start(orm.Context(), policy, event.Resource, task.ExecutionTriggerEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -87,12 +80,12 @@ func (h *handler) Handle(event *Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*model.Policy, error) {
|
||||
_, policies, err := h.policyCtl.List()
|
||||
func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*rep.Policy, error) {
|
||||
policies, err := replication.Ctl.ListPolicies(orm.Context(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := []*model.Policy{}
|
||||
result := []*rep.Policy{}
|
||||
for _, policy := range policies {
|
||||
// disabled
|
||||
if !policy.Enabled {
|
||||
@ -112,7 +105,7 @@ func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*model.Policy,
|
||||
continue
|
||||
}
|
||||
// doesn't replicate deletion
|
||||
if resource.Deleted && !policy.Deletion {
|
||||
if resource.Deleted && !policy.ReplicateDeletion {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -129,52 +122,3 @@ func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*model.Policy,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PopulateRegistries populates the source registry and destination registry properties for policy
|
||||
func PopulateRegistries(registryMgr registry.Manager, policy *model.Policy) error {
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
registry, err := getRegistry(registryMgr, policy.SrcRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policy.SrcRegistry = registry
|
||||
registry, err = getRegistry(registryMgr, policy.DestRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policy.DestRegistry = registry
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRegistry(registryMgr registry.Manager, registry *model.Registry) (*model.Registry, error) {
|
||||
if registry == nil || registry.ID == 0 {
|
||||
return GetLocalRegistry(), nil
|
||||
}
|
||||
reg, err := registryMgr.Get(registry.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if reg == nil {
|
||||
return nil, fmt.Errorf("registry %d not found", registry.ID)
|
||||
}
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// GetLocalRegistry returns the info of the local Harbor registry
|
||||
func GetLocalRegistry() *model.Registry {
|
||||
return &model.Registry{
|
||||
Type: model.RegistryTypeHarbor,
|
||||
Name: "Local",
|
||||
URL: config.Config.CoreURL,
|
||||
TokenServiceURL: config.Config.TokenServiceURL,
|
||||
Status: "healthy",
|
||||
Credential: &model.Credential{
|
||||
Type: model.CredentialTypeSecret,
|
||||
// use secret to do the auth for the local Harbor
|
||||
AccessSecret: config.Config.JobserviceSecret,
|
||||
},
|
||||
Insecure: !commonthttp.InternalTLSEnabled(),
|
||||
}
|
||||
}
|
||||
|
@ -1,305 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/testing/controller/replication"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/replication/config"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakedPolicyController struct{}
|
||||
|
||||
func (f *fakedPolicyController) Create(*model.Policy) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakedPolicyController) List(...*model.PolicyQuery) (int64, []*model.Policy, error) {
|
||||
polices := []*model.Policy{
|
||||
{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
Deletion: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeEventBased,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "test/*",
|
||||
},
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
// nil trigger
|
||||
{
|
||||
ID: 2,
|
||||
Enabled: true,
|
||||
Deletion: true,
|
||||
Trigger: nil,
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library/*",
|
||||
},
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
// doesn't replicate deletion
|
||||
{
|
||||
ID: 3,
|
||||
Enabled: true,
|
||||
Deletion: false,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeEventBased,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library/*",
|
||||
},
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
// replicate deletion
|
||||
{
|
||||
ID: 4,
|
||||
Enabled: true,
|
||||
Deletion: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeEventBased,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library/*",
|
||||
},
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
// disabled
|
||||
{
|
||||
ID: 5,
|
||||
Enabled: false,
|
||||
Deletion: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeEventBased,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library/*",
|
||||
},
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
// the source registry is not local Harbor
|
||||
{
|
||||
ID: 6,
|
||||
Enabled: true,
|
||||
Deletion: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeEventBased,
|
||||
},
|
||||
Filters: []*model.Filter{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Value: "library/*",
|
||||
},
|
||||
},
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
return int64(len(polices)), polices, nil
|
||||
}
|
||||
func (f *fakedPolicyController) Get(id int64) (*model.Policy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedPolicyController) GetByName(name string) (*model.Policy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedPolicyController) Update(*model.Policy) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedPolicyController) Remove(int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakedRegistryManager struct{}
|
||||
|
||||
func (f *fakedRegistryManager) Add(*model.Registry) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) List(query *q.Query) (int64, []*model.Registry, error) {
|
||||
return 0, nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Get(id int64) (*model.Registry, error) {
|
||||
return &model.Registry{
|
||||
ID: 1,
|
||||
Type: model.RegistryTypeHarbor,
|
||||
}, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) GetByName(name string) (*model.Registry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Update(*model.Registry, ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Remove(int64) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedRegistryManager) HealthCheck() error {
|
||||
return nil
|
||||
}
|
||||
func TestGetRelatedPolicies(t *testing.T) {
|
||||
handler := &handler{
|
||||
policyCtl: &fakedPolicyController{},
|
||||
}
|
||||
policies, err := handler.getRelatedPolicies(&model.Resource{
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
},
|
||||
Artifacts: []*model.Artifact{
|
||||
{
|
||||
Type: "image",
|
||||
Digest: "sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042",
|
||||
Tags: []string{"latest"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 2, len(policies))
|
||||
assert.Equal(t, int64(3), policies[0].ID)
|
||||
assert.Equal(t, int64(4), policies[1].ID)
|
||||
|
||||
policies, err = handler.getRelatedPolicies(&model.Resource{
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
},
|
||||
Artifacts: []*model.Artifact{
|
||||
{
|
||||
Type: "image",
|
||||
Digest: "sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042",
|
||||
Tags: []string{"latest"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Deleted: true,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 1, len(policies))
|
||||
assert.Equal(t, int64(4), policies[0].ID)
|
||||
}
|
||||
|
||||
func TestHandle(t *testing.T) {
|
||||
dao.PrepareTestForPostgresSQL()
|
||||
config.Config = &config.Configuration{}
|
||||
ctl := &replication.Controller{}
|
||||
ctl.On("Start", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
handler := &handler{
|
||||
policyCtl: &fakedPolicyController{},
|
||||
registryMgr: &fakedRegistryManager{},
|
||||
ctl: ctl,
|
||||
}
|
||||
|
||||
// nil event
|
||||
err := handler.Handle(nil)
|
||||
require.NotNil(t, err)
|
||||
|
||||
// nil vtags
|
||||
err = handler.Handle(&Event{
|
||||
Resource: &model.Resource{
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
},
|
||||
Vtags: []string{},
|
||||
},
|
||||
},
|
||||
Type: EventTypeArtifactPush,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
|
||||
// unsupported event type
|
||||
err = handler.Handle(&Event{
|
||||
Resource: &model.Resource{
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
},
|
||||
Vtags: []string{"latest"},
|
||||
},
|
||||
},
|
||||
Type: "unsupported",
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
|
||||
// push image
|
||||
err = handler.Handle(&Event{
|
||||
Resource: &model.Resource{
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
},
|
||||
Artifacts: []*model.Artifact{
|
||||
{
|
||||
Tags: []string{"latest"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Type: EventTypeArtifactPush,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
// delete image
|
||||
err = handler.Handle(&Event{
|
||||
Resource: &model.Resource{
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "library/hello-world",
|
||||
},
|
||||
Artifacts: []*model.Artifact{
|
||||
{
|
||||
Tags: []string{"latest"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Type: EventTypeArtifactDelete,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
}
|
@ -14,13 +14,6 @@
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/robfig/cron"
|
||||
"time"
|
||||
)
|
||||
|
||||
// const definition
|
||||
const (
|
||||
FilterTypeResource FilterType = "resource"
|
||||
@ -33,109 +26,6 @@ const (
|
||||
TriggerTypeEventBased TriggerType = "event_based"
|
||||
)
|
||||
|
||||
// Policy defines the structure of a replication policy
|
||||
type Policy struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Creator string `json:"creator"`
|
||||
// source
|
||||
SrcRegistry *Registry `json:"src_registry"`
|
||||
// destination
|
||||
DestRegistry *Registry `json:"dest_registry"`
|
||||
// Only support two dest namespace modes:
|
||||
// Put all the src resources to the one single dest namespace
|
||||
// or keep namespaces same with the source ones (under this case,
|
||||
// the DestNamespace should be set to empty)
|
||||
DestNamespace string `json:"dest_namespace"`
|
||||
// Filters
|
||||
Filters []*Filter `json:"filters"`
|
||||
// Trigger
|
||||
Trigger *Trigger `json:"trigger"`
|
||||
// Settings
|
||||
// TODO: rename the property name
|
||||
Deletion bool `json:"deletion"`
|
||||
// If override the image tag
|
||||
Override bool `json:"override"`
|
||||
// Operations
|
||||
Enabled bool `json:"enabled"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// Valid the policy
|
||||
func (p *Policy) Valid(v *validation.Validation) {
|
||||
if len(p.Name) == 0 {
|
||||
v.SetError("name", "cannot be empty")
|
||||
}
|
||||
var srcRegistryID, dstRegistryID int64
|
||||
if p.SrcRegistry != nil {
|
||||
srcRegistryID = p.SrcRegistry.ID
|
||||
}
|
||||
if p.DestRegistry != nil {
|
||||
dstRegistryID = p.DestRegistry.ID
|
||||
}
|
||||
|
||||
// one of the source registry and destination registry must be Harbor itself
|
||||
if srcRegistryID != 0 && dstRegistryID != 0 ||
|
||||
srcRegistryID == 0 && dstRegistryID == 0 {
|
||||
v.SetError("src_registry, dest_registry", "one of them should be empty and the other one shouldn't be empty")
|
||||
}
|
||||
|
||||
// valid the filters
|
||||
for _, filter := range p.Filters {
|
||||
switch filter.Type {
|
||||
case FilterTypeResource, FilterTypeName, FilterTypeTag:
|
||||
value, ok := filter.Value.(string)
|
||||
if !ok {
|
||||
v.SetError("filters", "the type of filter value isn't string")
|
||||
break
|
||||
}
|
||||
if filter.Type == FilterTypeResource {
|
||||
rt := ResourceType(value)
|
||||
if !(rt == ResourceTypeArtifact || rt == ResourceTypeImage || rt == ResourceTypeChart) {
|
||||
v.SetError("filters", fmt.Sprintf("invalid resource filter: %s", value))
|
||||
break
|
||||
}
|
||||
}
|
||||
case FilterTypeLabel:
|
||||
labels, ok := filter.Value.([]interface{})
|
||||
if !ok {
|
||||
v.SetError("filters", "the type of label filter value isn't string slice")
|
||||
break
|
||||
}
|
||||
for _, label := range labels {
|
||||
_, ok := label.(string)
|
||||
if !ok {
|
||||
v.SetError("filters", "the type of label filter value isn't string slice")
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
v.SetError("filters", "invalid filter type")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// valid trigger
|
||||
if p.Trigger != nil {
|
||||
switch p.Trigger.Type {
|
||||
case TriggerTypeManual, TriggerTypeEventBased:
|
||||
case TriggerTypeScheduled:
|
||||
if p.Trigger.Settings == nil || len(p.Trigger.Settings.Cron) == 0 {
|
||||
v.SetError("trigger", fmt.Sprintf("the cron string cannot be empty when the trigger type is %s", TriggerTypeScheduled))
|
||||
} else {
|
||||
_, err := cron.Parse(p.Trigger.Settings.Cron)
|
||||
if err != nil {
|
||||
v.SetError("trigger", fmt.Sprintf("invalid cron string for scheduled trigger: %s", p.Trigger.Settings.Cron))
|
||||
}
|
||||
}
|
||||
default:
|
||||
v.SetError("trigger", "invalid trigger type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FilterType represents the type info of the filter.
|
||||
type FilterType string
|
||||
|
||||
@ -158,15 +48,3 @@ type Trigger struct {
|
||||
type TriggerSettings struct {
|
||||
Cron string `json:"cron"`
|
||||
}
|
||||
|
||||
// PolicyQuery defines the query conditions for listing policies
|
||||
type PolicyQuery struct {
|
||||
Name string
|
||||
// TODO: need to consider how to support listing the policies
|
||||
// of one namespace in both pull and push modes
|
||||
Namespace string
|
||||
SrcRegistry int64
|
||||
DestRegistry int64
|
||||
Page int64
|
||||
Size int64
|
||||
}
|
||||
|
@ -1,225 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidOfPolicy(t *testing.T) {
|
||||
cases := []struct {
|
||||
policy *Policy
|
||||
pass bool
|
||||
}{
|
||||
// empty name
|
||||
{
|
||||
policy: &Policy{},
|
||||
pass: false,
|
||||
},
|
||||
// empty source registry and destination registry
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// source registry and destination registry both not empty
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 2,
|
||||
},
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// invalid filter
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*Filter{
|
||||
{
|
||||
Type: "invalid_type",
|
||||
},
|
||||
},
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// invalid filter
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*Filter{
|
||||
{
|
||||
Type: FilterTypeResource,
|
||||
Value: "invalid_resource_type",
|
||||
},
|
||||
},
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// invalid filter
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*Filter{
|
||||
{
|
||||
Type: FilterTypeResource,
|
||||
Value: ResourceTypeImage,
|
||||
},
|
||||
{
|
||||
Type: FilterTypeTag,
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// invalid trigger
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*Filter{
|
||||
{
|
||||
Type: FilterTypeName,
|
||||
Value: "library",
|
||||
},
|
||||
},
|
||||
Trigger: &Trigger{
|
||||
Type: "invalid_type",
|
||||
},
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// invalid trigger
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*Filter{
|
||||
{
|
||||
Type: FilterTypeName,
|
||||
Value: "library",
|
||||
},
|
||||
},
|
||||
Trigger: &Trigger{
|
||||
Type: TriggerTypeScheduled,
|
||||
},
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// invalid cron
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*Filter{
|
||||
{
|
||||
Type: FilterTypeResource,
|
||||
Value: "image",
|
||||
},
|
||||
{
|
||||
Type: FilterTypeName,
|
||||
Value: "library/**",
|
||||
},
|
||||
},
|
||||
Trigger: &Trigger{
|
||||
Type: TriggerTypeScheduled,
|
||||
Settings: &TriggerSettings{
|
||||
Cron: "* * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
pass: false,
|
||||
},
|
||||
// pass
|
||||
{
|
||||
policy: &Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistry: &Registry{
|
||||
ID: 0,
|
||||
},
|
||||
DestRegistry: &Registry{
|
||||
ID: 1,
|
||||
},
|
||||
Filters: []*Filter{
|
||||
{
|
||||
Type: FilterTypeResource,
|
||||
Value: "image",
|
||||
},
|
||||
{
|
||||
Type: FilterTypeName,
|
||||
Value: "library/**",
|
||||
},
|
||||
},
|
||||
Trigger: &Trigger{
|
||||
Type: TriggerTypeScheduled,
|
||||
Settings: &TriggerSettings{
|
||||
Cron: "* * * * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
pass: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
fmt.Printf("running case %d ...\n", i)
|
||||
v := &validation.Validation{}
|
||||
c.policy.Valid(v)
|
||||
assert.Equal(t, c.pass, len(v.Errors) == 0)
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
)
|
||||
|
||||
// Controller controls the replication policies
|
||||
type Controller interface {
|
||||
// Create new policy
|
||||
Create(*model.Policy) (int64, error)
|
||||
// List the policies, returns the total count, policy list and error
|
||||
List(...*model.PolicyQuery) (int64, []*model.Policy, error)
|
||||
// Get policy with specified ID
|
||||
Get(int64) (*model.Policy, error)
|
||||
// Get policy by the name
|
||||
GetByName(string) (*model.Policy, error)
|
||||
// Update the specified policy
|
||||
Update(policy *model.Policy) error
|
||||
// Remove the specified policy
|
||||
Remove(int64) error
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/replication/policy"
|
||||
"github.com/goharbor/harbor/src/replication/policy/manager"
|
||||
)
|
||||
|
||||
// const definitions
|
||||
const (
|
||||
CallbackFuncName = "REPLICATION_CALLBACK"
|
||||
)
|
||||
|
||||
// NewController returns a policy controller which can CURD and schedule policies
|
||||
func NewController() policy.Controller {
|
||||
mgr := manager.NewDefaultManager()
|
||||
ctl := &controller{
|
||||
scheduler: scheduler.Sched,
|
||||
}
|
||||
ctl.Controller = mgr
|
||||
return ctl
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
policy.Controller
|
||||
scheduler scheduler.Scheduler
|
||||
}
|
||||
|
||||
func (c *controller) Create(policy *model.Policy) (int64, error) {
|
||||
id, err := c.Controller.Create(policy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if isScheduledTrigger(policy) {
|
||||
extras := make(map[string]interface{})
|
||||
if _, err = c.scheduler.Schedule(orm.Context(), job.Replication, id, "", policy.Trigger.Settings.Cron, CallbackFuncName, id, extras); err != nil {
|
||||
log.Errorf("failed to schedule the policy %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (c *controller) Update(policy *model.Policy) error {
|
||||
origin, err := c.Controller.Get(policy.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if origin == nil {
|
||||
return fmt.Errorf("policy %d not found", policy.ID)
|
||||
}
|
||||
// if no need to reschedule the policy, just update it
|
||||
if !isScheduleTriggerChanged(origin, policy) {
|
||||
return c.Controller.Update(policy)
|
||||
}
|
||||
// need to reschedule the policy
|
||||
// unschedule first if needed
|
||||
if isScheduledTrigger(origin) {
|
||||
if err = c.scheduler.UnScheduleByVendor(orm.Context(), job.Replication, origin.ID); err != nil {
|
||||
return fmt.Errorf("failed to unschedule the policy %d: %v", origin.ID, err)
|
||||
}
|
||||
}
|
||||
// update the policy
|
||||
if err = c.Controller.Update(policy); err != nil {
|
||||
return err
|
||||
}
|
||||
// schedule again if needed
|
||||
if isScheduledTrigger(policy) {
|
||||
extras := make(map[string]interface{})
|
||||
if _, err = c.scheduler.Schedule(orm.Context(), job.Replication, policy.ID, "", policy.Trigger.Settings.Cron, CallbackFuncName, policy.ID, extras); err != nil {
|
||||
return fmt.Errorf("failed to schedule the policy %d: %v", policy.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) Remove(policyID int64) error {
|
||||
policy, err := c.Controller.Get(policyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policy == nil {
|
||||
return fmt.Errorf("policy %d not found", policyID)
|
||||
}
|
||||
if isScheduledTrigger(policy) {
|
||||
if err = c.scheduler.UnScheduleByVendor(orm.Context(), job.Replication, policyID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.Controller.Remove(policyID)
|
||||
}
|
||||
|
||||
func isScheduledTrigger(policy *model.Policy) bool {
|
||||
if policy == nil {
|
||||
return false
|
||||
}
|
||||
if !policy.Enabled {
|
||||
return false
|
||||
}
|
||||
if policy.Trigger == nil {
|
||||
return false
|
||||
}
|
||||
return policy.Trigger.Type == model.TriggerTypeScheduled
|
||||
}
|
||||
|
||||
func isScheduleTriggerChanged(origin, current *model.Policy) bool {
|
||||
o := isScheduledTrigger(origin)
|
||||
c := isScheduledTrigger(current)
|
||||
// both triggers are not scheduled
|
||||
if !o && !c {
|
||||
return false
|
||||
}
|
||||
// both triggers are scheduled
|
||||
if o && c {
|
||||
return origin.Trigger.Settings.Cron != current.Trigger.Settings.Cron
|
||||
}
|
||||
// one is scheduled but the other one isn't
|
||||
return true
|
||||
}
|
@ -1,342 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/testing/mock"
|
||||
"github.com/goharbor/harbor/src/testing/pkg/scheduler"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakedPolicyController struct {
|
||||
policy *model.Policy
|
||||
}
|
||||
|
||||
func (f *fakedPolicyController) Create(*model.Policy) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakedPolicyController) List(...*model.PolicyQuery) (int64, []*model.Policy, error) {
|
||||
return 0, nil, nil
|
||||
}
|
||||
func (f *fakedPolicyController) Get(id int64) (*model.Policy, error) {
|
||||
return f.policy, nil
|
||||
}
|
||||
func (f *fakedPolicyController) GetByName(name string) (*model.Policy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedPolicyController) Update(*model.Policy) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedPolicyController) Remove(int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestIsScheduledTrigger(t *testing.T) {
|
||||
cases := []struct {
|
||||
policy *model.Policy
|
||||
expected bool
|
||||
}{
|
||||
// policy is nil
|
||||
{
|
||||
policy: nil,
|
||||
expected: false,
|
||||
},
|
||||
// policy is disabled
|
||||
{
|
||||
policy: &model.Policy{
|
||||
Enabled: false,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
// trigger is nil
|
||||
{
|
||||
policy: &model.Policy{
|
||||
Enabled: true,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
// trigger type isn't scheduled
|
||||
{
|
||||
policy: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeManual,
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
// trigger type is scheduled
|
||||
{
|
||||
policy: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expected, isScheduledTrigger(c.policy))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsScheduleTriggerChanged(t *testing.T) {
|
||||
cases := []struct {
|
||||
origin *model.Policy
|
||||
current *model.Policy
|
||||
expected bool
|
||||
}{
|
||||
// both triggers are not scheduled
|
||||
{
|
||||
origin: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeManual,
|
||||
},
|
||||
},
|
||||
current: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeManual,
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
// both triggers are scheduled and the crons are not same
|
||||
{
|
||||
origin: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
current: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 * * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
// both triggers are scheduled and the crons are same
|
||||
{
|
||||
origin: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
current: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
// one trigger is scheduled but the other one isn't
|
||||
{
|
||||
origin: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
current: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeManual,
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
// one trigger is scheduled but disabled and
|
||||
// the other one is scheduled but enabled
|
||||
{
|
||||
origin: &model.Policy{
|
||||
Enabled: false,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
current: &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expected, isScheduleTriggerChanged(c.origin, c.current))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
dao.PrepareTestForPostgresSQL()
|
||||
scheduler := &scheduler.Scheduler{}
|
||||
ctl := &controller{
|
||||
scheduler: scheduler,
|
||||
}
|
||||
ctl.Controller = &fakedPolicyController{}
|
||||
|
||||
// not scheduled trigger
|
||||
_, err := ctl.Create(&model.Policy{})
|
||||
require.Nil(t, err)
|
||||
|
||||
// scheduled trigger
|
||||
scheduler.On("Schedule", mock.Anything, mock.Anything,
|
||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
_, err = ctl.Create(&model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
scheduler.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
scheduler := &scheduler.Scheduler{}
|
||||
c := &fakedPolicyController{}
|
||||
ctl := &controller{
|
||||
scheduler: scheduler,
|
||||
}
|
||||
ctl.Controller = c
|
||||
|
||||
var origin, current *model.Policy
|
||||
// origin policy is nil
|
||||
current = &model.Policy{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
}
|
||||
err := ctl.Update(current)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// the trigger doesn't change
|
||||
origin = &model.Policy{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
}
|
||||
c.policy = origin
|
||||
current = origin
|
||||
err = ctl.Update(current)
|
||||
require.Nil(t, err)
|
||||
|
||||
// the trigger changed
|
||||
scheduler.On("Schedule", mock.Anything, mock.Anything,
|
||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||
scheduler.On("UnScheduleByVendor", mock.Anything, mock.Anything,
|
||||
mock.Anything).Return(nil)
|
||||
|
||||
origin = &model.Policy{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
}
|
||||
c.policy = origin
|
||||
current = &model.Policy{
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 * * * *",
|
||||
},
|
||||
},
|
||||
}
|
||||
err = ctl.Update(current)
|
||||
require.Nil(t, err)
|
||||
scheduler.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
scheduler := &scheduler.Scheduler{}
|
||||
c := &fakedPolicyController{}
|
||||
ctl := &controller{
|
||||
scheduler: scheduler,
|
||||
}
|
||||
ctl.Controller = c
|
||||
|
||||
// policy is nil
|
||||
err := ctl.Remove(1)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// the trigger type isn't scheduled
|
||||
policy := &model.Policy{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeManual,
|
||||
},
|
||||
}
|
||||
c.policy = policy
|
||||
err = ctl.Remove(1)
|
||||
require.Nil(t, err)
|
||||
|
||||
// the trigger type is scheduled
|
||||
scheduler.On("UnScheduleByVendor", mock.Anything, mock.Anything,
|
||||
mock.Anything).Return(nil)
|
||||
policy = &model.Policy{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{
|
||||
Type: model.TriggerTypeScheduled,
|
||||
Settings: &model.TriggerSettings{
|
||||
Cron: "03 05 * * *",
|
||||
},
|
||||
},
|
||||
}
|
||||
c.policy = policy
|
||||
err = ctl.Remove(1)
|
||||
require.Nil(t, err)
|
||||
scheduler.AssertExpectations(t)
|
||||
}
|
@ -1,332 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/replication/dao"
|
||||
persist_models "github.com/goharbor/harbor/src/replication/dao/models"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/replication/policy"
|
||||
)
|
||||
|
||||
var errNilPolicyModel = errors.New("nil policy model")
|
||||
|
||||
func convertFromPersistModel(policy *persist_models.RepPolicy) (*model.Policy, error) {
|
||||
if policy == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ply := model.Policy{
|
||||
ID: policy.ID,
|
||||
Name: policy.Name,
|
||||
Description: policy.Description,
|
||||
Creator: policy.Creator,
|
||||
DestNamespace: policy.DestNamespace,
|
||||
Deletion: policy.ReplicateDeletion,
|
||||
Override: policy.Override,
|
||||
Enabled: policy.Enabled,
|
||||
CreationTime: policy.CreationTime,
|
||||
UpdateTime: policy.UpdateTime,
|
||||
}
|
||||
if policy.SrcRegistryID > 0 {
|
||||
ply.SrcRegistry = &model.Registry{
|
||||
ID: policy.SrcRegistryID,
|
||||
}
|
||||
}
|
||||
if policy.DestRegistryID > 0 {
|
||||
ply.DestRegistry = &model.Registry{
|
||||
ID: policy.DestRegistryID,
|
||||
}
|
||||
}
|
||||
|
||||
// parse Filters
|
||||
filters, err := parseFilters(policy.Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ply.Filters = filters
|
||||
|
||||
// parse Trigger
|
||||
trigger, err := parseTrigger(policy.Trigger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ply.Trigger = trigger
|
||||
|
||||
return &ply, nil
|
||||
}
|
||||
|
||||
func convertToPersistModel(policy *model.Policy) (*persist_models.RepPolicy, error) {
|
||||
if policy == nil {
|
||||
return nil, errNilPolicyModel
|
||||
}
|
||||
|
||||
ply := &persist_models.RepPolicy{
|
||||
ID: policy.ID,
|
||||
Name: policy.Name,
|
||||
Description: policy.Description,
|
||||
Creator: policy.Creator,
|
||||
DestNamespace: policy.DestNamespace,
|
||||
Override: policy.Override,
|
||||
Enabled: policy.Enabled,
|
||||
ReplicateDeletion: policy.Deletion,
|
||||
CreationTime: policy.CreationTime,
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if policy.SrcRegistry != nil {
|
||||
ply.SrcRegistryID = policy.SrcRegistry.ID
|
||||
}
|
||||
if policy.DestRegistry != nil {
|
||||
ply.DestRegistryID = policy.DestRegistry.ID
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// DefaultManager provides replication policy CURD capabilities.
|
||||
type DefaultManager struct{}
|
||||
|
||||
var _ policy.Controller = &DefaultManager{}
|
||||
|
||||
// NewDefaultManager is the constructor of DefaultManager.
|
||||
func NewDefaultManager() *DefaultManager {
|
||||
return &DefaultManager{}
|
||||
}
|
||||
|
||||
// Create 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) Create(policy *model.Policy) (int64, error) {
|
||||
ply, err := convertToPersistModel(policy)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return dao.AddRepPolicy(ply)
|
||||
}
|
||||
|
||||
// List returns all the policies
|
||||
func (m *DefaultManager) List(queries ...*model.PolicyQuery) (total int64, policies []*model.Policy, err error) {
|
||||
var persistPolicies []*persist_models.RepPolicy
|
||||
total, persistPolicies, err = dao.GetPolicies(queries...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, policy := range persistPolicies {
|
||||
ply, err := convertFromPersistModel(policy)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
policies = append(policies, ply)
|
||||
}
|
||||
|
||||
if policies == nil {
|
||||
policies = []*model.Policy{}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get returns the policy with the specified ID
|
||||
func (m *DefaultManager) Get(policyID int64) (*model.Policy, error) {
|
||||
policy, err := dao.GetRepPolicy(policyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertFromPersistModel(policy)
|
||||
}
|
||||
|
||||
// GetByName returns the policy with the specified name
|
||||
func (m *DefaultManager) GetByName(name string) (*model.Policy, error) {
|
||||
policy, err := dao.GetRepPolicyByName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertFromPersistModel(policy)
|
||||
}
|
||||
|
||||
// Update Update the specified policy
|
||||
func (m *DefaultManager) Update(policy *model.Policy) error {
|
||||
updatePolicy, err := convertToPersistModel(policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.UpdateRepPolicy(updatePolicy)
|
||||
}
|
||||
|
||||
// Remove Remove the specified policy
|
||||
func (m *DefaultManager) Remove(policyID int64) error {
|
||||
return dao.DeleteRepPolicy(policyID)
|
||||
}
|
||||
|
||||
type filter struct {
|
||||
Type model.FilterType `json:"type"`
|
||||
Value interface{} `json:"value"`
|
||||
Kind string `json:"kind"`
|
||||
Pattern string `json:"pattern"`
|
||||
}
|
||||
|
||||
type trigger struct {
|
||||
Type model.TriggerType `json:"type"`
|
||||
Settings *model.TriggerSettings `json:"trigger_settings"`
|
||||
Kind string `json:"kind"`
|
||||
ScheduleParam *scheduleParam `json:"schedule_param"`
|
||||
}
|
||||
|
||||
type scheduleParam struct {
|
||||
Type string `json:"type"`
|
||||
Weekday int8 `json:"weekday"`
|
||||
Offtime int64 `json:"offtime"`
|
||||
}
|
||||
|
||||
func parseFilters(str string) ([]*model.Filter, error) {
|
||||
if len(str) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
items := []*filter{}
|
||||
if err := json.Unmarshal([]byte(str), &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filters := []*model.Filter{}
|
||||
for _, item := range items {
|
||||
filter := &model.Filter{
|
||||
Type: item.Type,
|
||||
Value: item.Value,
|
||||
}
|
||||
// keep backwards compatibility
|
||||
if len(filter.Type) == 0 {
|
||||
if filter.Value == nil {
|
||||
filter.Value = item.Pattern
|
||||
}
|
||||
switch item.Kind {
|
||||
case "repository":
|
||||
// a name filter "project_name/**" must exist after running upgrade
|
||||
// if there is any repository filter, merge it into the name filter
|
||||
repository, ok := filter.Value.(string)
|
||||
if ok && len(repository) > 0 {
|
||||
for _, item := range items {
|
||||
if item.Type == model.FilterTypeName {
|
||||
name, ok := item.Value.(string)
|
||||
if ok && len(name) > 0 {
|
||||
item.Value = strings.Replace(name, "**", repository, 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
case "tag":
|
||||
filter.Type = model.FilterTypeTag
|
||||
case "label":
|
||||
// drop all legend label filters
|
||||
continue
|
||||
default:
|
||||
log.Warningf("unknown filter type: %s", filter.Type)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// convert the type of value from string to model.ResourceType if the filter
|
||||
// is a resource type filter
|
||||
if filter.Type == model.FilterTypeResource {
|
||||
filter.Value = (model.ResourceType)(filter.Value.(string))
|
||||
}
|
||||
if filter.Type == model.FilterTypeLabel {
|
||||
labels := []string{}
|
||||
for _, label := range filter.Value.([]interface{}) {
|
||||
labels = append(labels, label.(string))
|
||||
}
|
||||
filter.Value = labels
|
||||
}
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func parseTrigger(str string) (*model.Trigger, error) {
|
||||
if len(str) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
item := &trigger{}
|
||||
if err := json.Unmarshal([]byte(str), item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trigger := &model.Trigger{
|
||||
Type: item.Type,
|
||||
Settings: item.Settings,
|
||||
}
|
||||
// keep backwards compatibility
|
||||
if len(trigger.Type) == 0 {
|
||||
switch item.Kind {
|
||||
case "Manual":
|
||||
trigger.Type = model.TriggerTypeManual
|
||||
case "Immediate":
|
||||
trigger.Type = model.TriggerTypeEventBased
|
||||
case "Scheduled":
|
||||
trigger.Type = model.TriggerTypeScheduled
|
||||
trigger.Settings = &model.TriggerSettings{
|
||||
Cron: parseScheduleParamToCron(item.ScheduleParam),
|
||||
}
|
||||
default:
|
||||
log.Warningf("unknown trigger type: %s", item.Kind)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
||||
func parseScheduleParamToCron(param *scheduleParam) string {
|
||||
if param == nil {
|
||||
return ""
|
||||
}
|
||||
offtime := param.Offtime
|
||||
offtime = offtime % (3600 * 24)
|
||||
hour := int(offtime / 3600)
|
||||
offtime = offtime % 3600
|
||||
minute := int(offtime / 60)
|
||||
second := int(offtime % 60)
|
||||
if param.Type == "Weekly" {
|
||||
return fmt.Sprintf("%d %d %d * * %d", second, minute, hour, param.Weekday%7)
|
||||
}
|
||||
return fmt.Sprintf("%d %d %d * * *", second, minute, hour)
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
persist_models "github.com/goharbor/harbor/src/replication/dao/models"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_convertFromPersistModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
from *persist_models.RepPolicy
|
||||
want *model.Policy
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Nil Persist Model",
|
||||
from: nil,
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "parse Filters Error",
|
||||
from: &persist_models.RepPolicy{Filters: "abc"},
|
||||
want: nil, wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "parse Trigger Error",
|
||||
from: &persist_models.RepPolicy{Trigger: "abc"},
|
||||
want: nil, wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Persist Model", from: &persist_models.RepPolicy{
|
||||
ID: 999,
|
||||
Name: "Policy Test",
|
||||
Description: "Policy Description",
|
||||
Creator: "someone",
|
||||
SrcRegistryID: 123,
|
||||
DestRegistryID: 456,
|
||||
DestNamespace: "target_ns",
|
||||
ReplicateDeletion: true,
|
||||
Override: true,
|
||||
Enabled: true,
|
||||
Trigger: "",
|
||||
Filters: "[]",
|
||||
}, want: &model.Policy{
|
||||
ID: 999,
|
||||
Name: "Policy Test",
|
||||
Description: "Policy Description",
|
||||
Creator: "someone",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 123,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 456,
|
||||
},
|
||||
DestNamespace: "target_ns",
|
||||
Deletion: true,
|
||||
Override: true,
|
||||
Enabled: true,
|
||||
Trigger: nil,
|
||||
Filters: []*model.Filter{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := convertFromPersistModel(tt.from)
|
||||
|
||||
if tt.wantErr {
|
||||
require.NotNil(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.want == nil {
|
||||
require.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err, tt.name)
|
||||
assert.Equal(t, tt.want.ID, got.ID)
|
||||
assert.Equal(t, tt.want.Name, got.Name)
|
||||
assert.Equal(t, tt.want.Description, got.Description)
|
||||
assert.Equal(t, tt.want.Creator, got.Creator)
|
||||
assert.Equal(t, tt.want.SrcRegistry.ID, got.SrcRegistry.ID)
|
||||
assert.Equal(t, tt.want.DestRegistry.ID, got.DestRegistry.ID)
|
||||
assert.Equal(t, tt.want.DestNamespace, got.DestNamespace)
|
||||
assert.Equal(t, tt.want.Deletion, got.Deletion)
|
||||
assert.Equal(t, tt.want.Override, got.Override)
|
||||
assert.Equal(t, tt.want.Enabled, got.Enabled)
|
||||
assert.Equal(t, tt.want.Trigger, got.Trigger)
|
||||
assert.Equal(t, tt.want.Filters, got.Filters)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertToPersistModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
from *model.Policy
|
||||
want *persist_models.RepPolicy
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Nil Model", from: nil, want: nil, wantErr: true},
|
||||
{
|
||||
name: "Persist Model", from: &model.Policy{
|
||||
ID: 999,
|
||||
Name: "Policy Test",
|
||||
Description: "Policy Description",
|
||||
Creator: "someone",
|
||||
SrcRegistry: &model.Registry{
|
||||
ID: 123,
|
||||
},
|
||||
DestRegistry: &model.Registry{
|
||||
ID: 456,
|
||||
},
|
||||
DestNamespace: "target_ns",
|
||||
Deletion: true,
|
||||
Override: true,
|
||||
Enabled: true,
|
||||
Trigger: &model.Trigger{},
|
||||
Filters: []*model.Filter{{Type: "registry", Value: "abc"}},
|
||||
}, want: &persist_models.RepPolicy{
|
||||
ID: 999,
|
||||
Name: "Policy Test",
|
||||
Description: "Policy Description",
|
||||
Creator: "someone",
|
||||
SrcRegistryID: 123,
|
||||
DestRegistryID: 456,
|
||||
DestNamespace: "target_ns",
|
||||
ReplicateDeletion: true,
|
||||
Override: true,
|
||||
Enabled: true,
|
||||
Trigger: "{\"type\":\"\",\"trigger_settings\":null}",
|
||||
Filters: "[{\"type\":\"registry\",\"value\":\"abc\"}]",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := convertToPersistModel(tt.from)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Equal(t, err, errNilPolicyModel)
|
||||
return
|
||||
}
|
||||
|
||||
require.Nil(t, err, tt.name)
|
||||
assert.Equal(t, tt.want.ID, got.ID)
|
||||
assert.Equal(t, tt.want.Name, got.Name)
|
||||
assert.Equal(t, tt.want.Description, got.Description)
|
||||
assert.Equal(t, tt.want.Creator, got.Creator)
|
||||
assert.Equal(t, tt.want.SrcRegistryID, got.SrcRegistryID)
|
||||
assert.Equal(t, tt.want.DestRegistryID, got.DestRegistryID)
|
||||
assert.Equal(t, tt.want.DestNamespace, got.DestNamespace)
|
||||
assert.Equal(t, tt.want.ReplicateDeletion, got.ReplicateDeletion)
|
||||
assert.Equal(t, tt.want.Override, got.Override)
|
||||
assert.Equal(t, tt.want.Enabled, got.Enabled)
|
||||
assert.Equal(t, tt.want.Trigger, got.Trigger)
|
||||
assert.Equal(t, tt.want.Filters, got.Filters)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDefaultManager(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want *DefaultManager
|
||||
}{
|
||||
{want: &DefaultManager{}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := NewDefaultManager(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NewDefaultManager() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFilters(t *testing.T) {
|
||||
// nil filter string
|
||||
str := ""
|
||||
filters, err := parseFilters(str)
|
||||
require.Nil(t, err)
|
||||
assert.Nil(t, filters)
|
||||
// only contains the fields that introduced in the latest version
|
||||
str = `[{"type":"name","value":"library/hello-world"}]`
|
||||
filters, err = parseFilters(str)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(filters))
|
||||
assert.Equal(t, model.FilterTypeName, filters[0].Type)
|
||||
assert.Equal(t, "library/hello-world", filters[0].Value.(string))
|
||||
// contains "kind" from previous versions
|
||||
str = `[{"kind":"repository","value":"hello-world"},{"type":"name","value":"library/**"}]`
|
||||
filters, err = parseFilters(str)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(filters))
|
||||
assert.Equal(t, model.FilterTypeName, filters[0].Type)
|
||||
assert.Equal(t, "library/hello-world", filters[0].Value.(string))
|
||||
// contains "pattern" from previous versions
|
||||
str = `[{"kind":"repository","pattern":"hello-world"},{"type":"name","value":"library/**"}]`
|
||||
filters, err = parseFilters(str)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(filters))
|
||||
assert.Equal(t, model.FilterTypeName, filters[0].Type)
|
||||
assert.Equal(t, "library/hello-world", filters[0].Value.(string))
|
||||
}
|
||||
|
||||
func TestParseTrigger(t *testing.T) {
|
||||
// nil trigger string
|
||||
str := ""
|
||||
trigger, err := parseTrigger(str)
|
||||
require.Nil(t, err)
|
||||
assert.Nil(t, trigger)
|
||||
// only contains the fields that introduced in the latest version
|
||||
str = `{"type":"scheduled", "trigger_settings":{"cron":"1 * * * * *"}}`
|
||||
trigger, err = parseTrigger(str)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, model.TriggerTypeScheduled, trigger.Type)
|
||||
assert.Equal(t, "1 * * * * *", trigger.Settings.Cron)
|
||||
// contains "kind" from previous versions
|
||||
str = `{"kind":"Manual"}`
|
||||
trigger, err = parseTrigger(str)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, model.TriggerTypeManual, trigger.Type)
|
||||
assert.Nil(t, trigger.Settings)
|
||||
// contains "kind" from previous versions
|
||||
str = `{"kind":"Immediate"}`
|
||||
trigger, err = parseTrigger(str)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, model.TriggerTypeEventBased, trigger.Type)
|
||||
assert.Nil(t, trigger.Settings)
|
||||
// contains "schedule_param" from previous versions
|
||||
str = `{"kind":"Scheduled","schedule_param":{"type":"Weekly","weekday":1,"offtime":0}}`
|
||||
trigger, err = parseTrigger(str)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, model.TriggerTypeScheduled, trigger.Type)
|
||||
assert.Equal(t, "0 0 0 * * 1", trigger.Settings.Cron)
|
||||
// contains "schedule_param" from previous versions
|
||||
str = `{"kind":"Scheduled","schedule_param":{"type":"Daily","offtime":0}}`
|
||||
trigger, err = parseTrigger(str)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, model.TriggerTypeScheduled, trigger.Type)
|
||||
assert.Equal(t, "0 0 0 * * *", trigger.Settings.Cron)
|
||||
}
|
@ -69,7 +69,7 @@ func (m *DefaultManager) Get(id int64) (*model.Registry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return fromDaoModel(registry)
|
||||
return FromDaoModel(registry)
|
||||
}
|
||||
|
||||
// GetByName gets a registry by its name
|
||||
@ -83,7 +83,7 @@ func (m *DefaultManager) GetByName(name string) (*model.Registry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return fromDaoModel(registry)
|
||||
return FromDaoModel(registry)
|
||||
}
|
||||
|
||||
// List lists registries according to query provided.
|
||||
@ -95,7 +95,7 @@ func (m *DefaultManager) List(query *q.Query) (int64, []*model.Registry, error)
|
||||
|
||||
var results []*model.Registry
|
||||
for _, r := range registries {
|
||||
registry, err := fromDaoModel(r)
|
||||
registry, err := FromDaoModel(r)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@ -107,7 +107,7 @@ func (m *DefaultManager) List(query *q.Query) (int64, []*model.Registry, error)
|
||||
|
||||
// Add adds a new registry
|
||||
func (m *DefaultManager) Add(registry *model.Registry) (int64, error) {
|
||||
r, err := toDaoModel(registry)
|
||||
r, err := ToDaoModel(registry)
|
||||
if err != nil {
|
||||
log.Errorf("Convert registry model to dao layer model error: %v", err)
|
||||
return -1, err
|
||||
@ -124,7 +124,7 @@ func (m *DefaultManager) Add(registry *model.Registry) (int64, error) {
|
||||
|
||||
// Update updates a registry
|
||||
func (m *DefaultManager) Update(registry *model.Registry, props ...string) error {
|
||||
r, err := toDaoModel(registry)
|
||||
r, err := ToDaoModel(registry)
|
||||
if err != nil {
|
||||
log.Errorf("Convert registry model to dao layer model error: %v", err)
|
||||
return err
|
||||
@ -219,9 +219,9 @@ func encrypt(secret string) (string, error) {
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// fromDaoModel converts DAO layer registry model to replication model.
|
||||
// FromDaoModel converts DAO layer registry model to replication model.
|
||||
// Also, if access secret is provided, decrypt it.
|
||||
func fromDaoModel(registry *models.Registry) (*model.Registry, error) {
|
||||
func FromDaoModel(registry *models.Registry) (*model.Registry, error) {
|
||||
r := &model.Registry{
|
||||
ID: registry.ID,
|
||||
Name: registry.Name,
|
||||
@ -254,9 +254,9 @@ func fromDaoModel(registry *models.Registry) (*model.Registry, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// toDaoModel converts registry model from replication to DAO layer model.
|
||||
// ToDaoModel converts registry model from replication to DAO layer model.
|
||||
// Also, if access secret is provided, encrypt it.
|
||||
func toDaoModel(registry *model.Registry) (*models.Registry, error) {
|
||||
func ToDaoModel(registry *model.Registry) (*models.Registry, error) {
|
||||
m := &models.Registry{
|
||||
ID: registry.ID,
|
||||
URL: registry.URL,
|
||||
|
@ -15,20 +15,12 @@
|
||||
package replication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/replication"
|
||||
cfg "github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/scheduler"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/goharbor/harbor/src/replication/config"
|
||||
"github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/policy"
|
||||
"github.com/goharbor/harbor/src/replication/policy/controller"
|
||||
"github.com/goharbor/harbor/src/replication/registry"
|
||||
|
||||
// register the Harbor adapter
|
||||
@ -66,41 +58,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// PolicyCtl is a global policy controller
|
||||
PolicyCtl policy.Controller
|
||||
// RegistryMgr is a global registry manager
|
||||
RegistryMgr registry.Manager
|
||||
// EventHandler handles images/chart pull/push events
|
||||
EventHandler event.Handler
|
||||
)
|
||||
|
||||
func init() {
|
||||
callbackFunc := func(ctx context.Context, param string) error {
|
||||
policyID, err := strconv.ParseInt(param, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policy, err := PolicyCtl.Get(policyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policy == nil {
|
||||
return fmt.Errorf("policy %d not found", policyID)
|
||||
}
|
||||
if err = event.PopulateRegistries(RegistryMgr, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = replication.Ctl.Start(ctx, policy, nil, task.ExecutionTriggerSchedule)
|
||||
return err
|
||||
}
|
||||
err := scheduler.RegisterCallbackFunc(controller.CallbackFuncName, callbackFunc)
|
||||
if err != nil {
|
||||
log.Errorf("failed to register the callback function for replication: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Init the global variables and configurations
|
||||
func Init(closing, done chan struct{}) error {
|
||||
// init config
|
||||
@ -116,10 +79,8 @@ func Init(closing, done chan struct{}) error {
|
||||
}
|
||||
// init registry manager
|
||||
RegistryMgr = registry.NewDefaultManager()
|
||||
// init policy controller
|
||||
PolicyCtl = controller.NewController()
|
||||
// init event handler
|
||||
EventHandler = event.NewHandler(PolicyCtl, RegistryMgr)
|
||||
EventHandler = event.NewHandler(RegistryMgr)
|
||||
log.Debug("the replication initialization completed")
|
||||
|
||||
// Start health checker for registries
|
||||
|
@ -36,7 +36,6 @@ func TestInit(t *testing.T) {
|
||||
config.InitWithSettings(nil)
|
||||
err = Init(make(chan struct{}), make(chan struct{}))
|
||||
require.Nil(t, err)
|
||||
assert.NotNil(t, PolicyCtl)
|
||||
assert.NotNil(t, RegistryMgr)
|
||||
assert.NotNil(t, EventHandler)
|
||||
}
|
||||
|
@ -56,14 +56,22 @@ func (*BaseAPI) SendError(ctx context.Context, err error) middleware.Responder {
|
||||
return NewErrResponder(err)
|
||||
}
|
||||
|
||||
// HasPermission returns true when the request has action permission on resource
|
||||
func (*BaseAPI) HasPermission(ctx context.Context, action rbac.Action, resource rbac.Resource) bool {
|
||||
s, ok := security.FromContext(ctx)
|
||||
// GetSecurityContext from the provided context
|
||||
func (*BaseAPI) GetSecurityContext(ctx context.Context) (security.Context, error) {
|
||||
sc, ok := security.FromContext(ctx)
|
||||
if !ok {
|
||||
log.Warningf("security not found in the context")
|
||||
return nil, errors.UnauthorizedError(errors.New("security context not found"))
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// HasPermission returns true when the request has action permission on resource
|
||||
func (b *BaseAPI) HasPermission(ctx context.Context, action rbac.Action, resource rbac.Resource) bool {
|
||||
s, err := b.GetSecurityContext(ctx)
|
||||
if err != nil {
|
||||
log.Warningf("security context not found")
|
||||
return false
|
||||
}
|
||||
|
||||
return s.Can(ctx, action, resource)
|
||||
}
|
||||
|
||||
@ -98,9 +106,9 @@ func (b *BaseAPI) RequireProjectAccess(ctx context.Context, projectIDOrName inte
|
||||
if b.HasProjectPermission(ctx, projectIDOrName, action, subresource...) {
|
||||
return nil
|
||||
}
|
||||
secCtx, ok := security.FromContext(ctx)
|
||||
if !ok {
|
||||
return errors.UnauthorizedError(errors.New("security context not found"))
|
||||
secCtx, err := b.GetSecurityContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !secCtx.IsAuthenticated() {
|
||||
return errors.UnauthorizedError(nil)
|
||||
@ -110,9 +118,9 @@ func (b *BaseAPI) RequireProjectAccess(ctx context.Context, projectIDOrName inte
|
||||
|
||||
// RequireSystemAccess checks the system admin permission according to the security context
|
||||
func (b *BaseAPI) RequireSystemAccess(ctx context.Context, action rbac.Action, subresource ...rbac.Resource) error {
|
||||
secCtx, ok := security.FromContext(ctx)
|
||||
if !ok {
|
||||
return errors.UnauthorizedError(errors.New("security context not found"))
|
||||
secCtx, err := b.GetSecurityContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !secCtx.IsAuthenticated() {
|
||||
return errors.UnauthorizedError(nil)
|
||||
@ -126,9 +134,9 @@ func (b *BaseAPI) RequireSystemAccess(ctx context.Context, action rbac.Action, s
|
||||
|
||||
// RequireAuthenticated checks it's authenticated according to the security context
|
||||
func (b *BaseAPI) RequireAuthenticated(ctx context.Context) error {
|
||||
secCtx, ok := security.FromContext(ctx)
|
||||
if !ok {
|
||||
return errors.UnauthorizedError(errors.New("security context not found"))
|
||||
secCtx, err := b.GetSecurityContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !secCtx.IsAuthenticated() {
|
||||
return errors.UnauthorizedError(nil)
|
||||
|
@ -16,59 +16,198 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/replication"
|
||||
"github.com/goharbor/harbor/src/jobservice/job"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
rep "github.com/goharbor/harbor/src/pkg/replication"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
replica "github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/policy"
|
||||
"github.com/goharbor/harbor/src/replication/policy/manager"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/replication"
|
||||
)
|
||||
|
||||
func newReplicationAPI() *replicationAPI {
|
||||
return &replicationAPI{
|
||||
ctl: replication.Ctl,
|
||||
policyMgr: manager.NewDefaultManager(),
|
||||
ctl: replication.Ctl,
|
||||
}
|
||||
}
|
||||
|
||||
type replicationAPI struct {
|
||||
BaseAPI
|
||||
ctl replication.Controller
|
||||
policyMgr policy.Controller
|
||||
ctl replication.Controller
|
||||
}
|
||||
|
||||
func (r *replicationAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *replicationAPI) StartReplication(ctx context.Context, params operation.StartReplicationParams) middleware.Responder {
|
||||
// TODO move the following logic to the replication controller after refactoring the policy management part with the new programming model
|
||||
if err := r.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceReplication); err != nil {
|
||||
func (r *replicationAPI) CreateReplicationPolicy(ctx context.Context, params operation.CreateReplicationPolicyParams) middleware.Responder {
|
||||
if err := r.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceReplicationPolicy); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
policy, err := r.policyMgr.Get(params.Execution.PolicyID)
|
||||
sc, err := r.GetSecurityContext(ctx)
|
||||
if err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
if policy == nil {
|
||||
return r.SendError(ctx, errors.New(nil).WithCode(errors.NotFoundCode).
|
||||
WithMessage("the replication policy %d not found", params.Execution.PolicyID))
|
||||
policy := &rep.Policy{
|
||||
Name: params.Policy.Name,
|
||||
Description: params.Policy.Description,
|
||||
Creator: sc.GetUsername(),
|
||||
DestNamespace: params.Policy.DestNamespace,
|
||||
ReplicateDeletion: params.Policy.Deletion,
|
||||
Override: params.Policy.Override,
|
||||
Enabled: params.Policy.Enabled,
|
||||
}
|
||||
if err = event.PopulateRegistries(replica.RegistryMgr, policy); err != nil {
|
||||
if params.Policy.SrcRegistry != nil {
|
||||
policy.SrcRegistry = &model.Registry{
|
||||
ID: params.Policy.SrcRegistry.ID,
|
||||
}
|
||||
}
|
||||
if params.Policy.DestRegistry != nil {
|
||||
policy.DestRegistry = &model.Registry{
|
||||
ID: params.Policy.DestRegistry.ID,
|
||||
}
|
||||
}
|
||||
if len(params.Policy.Filters) > 0 {
|
||||
for _, filter := range params.Policy.Filters {
|
||||
policy.Filters = append(policy.Filters, &model.Filter{
|
||||
Type: model.FilterType(filter.Type),
|
||||
Value: filter.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if params.Policy.Trigger != nil {
|
||||
policy.Trigger = &model.Trigger{
|
||||
Type: model.TriggerType(params.Policy.Trigger.Type),
|
||||
}
|
||||
if params.Policy.Trigger.TriggerSettings != nil {
|
||||
policy.Trigger.Settings = &model.TriggerSettings{
|
||||
Cron: params.Policy.Trigger.TriggerSettings.Cron,
|
||||
}
|
||||
}
|
||||
}
|
||||
id, err := r.ctl.CreatePolicy(ctx, policy)
|
||||
if err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), id)
|
||||
return operation.NewCreateReplicationPolicyCreated().WithLocation(location)
|
||||
}
|
||||
|
||||
func (r *replicationAPI) UpdateReplicationPolicy(ctx context.Context, params operation.UpdateReplicationPolicyParams) middleware.Responder {
|
||||
if err := r.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceReplicationPolicy); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
policy := &rep.Policy{
|
||||
ID: params.ID,
|
||||
Name: params.Policy.Name,
|
||||
Description: params.Policy.Description,
|
||||
ReplicateDeletion: params.Policy.Deletion,
|
||||
Override: params.Policy.Override,
|
||||
Enabled: params.Policy.Enabled,
|
||||
}
|
||||
if params.Policy.SrcRegistry != nil {
|
||||
policy.SrcRegistry = &model.Registry{
|
||||
ID: params.Policy.SrcRegistry.ID,
|
||||
}
|
||||
}
|
||||
if params.Policy.DestRegistry != nil {
|
||||
policy.DestRegistry = &model.Registry{
|
||||
ID: params.Policy.DestRegistry.ID,
|
||||
}
|
||||
}
|
||||
if len(params.Policy.Filters) > 0 {
|
||||
for _, filter := range params.Policy.Filters {
|
||||
policy.Filters = append(policy.Filters, &model.Filter{
|
||||
Type: model.FilterType(filter.Type),
|
||||
Value: filter.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if params.Policy.Trigger != nil {
|
||||
policy.Trigger = &model.Trigger{
|
||||
Type: model.TriggerType(params.Policy.Trigger.Type),
|
||||
}
|
||||
if params.Policy.Trigger.TriggerSettings != nil {
|
||||
policy.Trigger.Settings = &model.TriggerSettings{
|
||||
Cron: params.Policy.Trigger.TriggerSettings.Cron,
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := r.ctl.UpdatePolicy(ctx, policy); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewUpdateReplicationPolicyOK()
|
||||
}
|
||||
|
||||
func (r *replicationAPI) ListReplicationPolicies(ctx context.Context, params operation.ListReplicationPoliciesParams) middleware.Responder {
|
||||
if err := r.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceReplicationPolicy); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
query, err := r.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
|
||||
if err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
if params.Name != nil {
|
||||
query.Keywords["Name"] = &q.FuzzyMatchValue{
|
||||
Value: *params.Name,
|
||||
}
|
||||
}
|
||||
total, err := r.ctl.PolicyCount(ctx, query)
|
||||
if err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
policies, err := r.ctl.ListPolicies(ctx, query)
|
||||
if err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
var result []*models.ReplicationPolicy
|
||||
for _, policy := range policies {
|
||||
result = append(result, convertReplicationPolicy(policy))
|
||||
}
|
||||
return operation.NewListReplicationPoliciesOK().
|
||||
WithXTotalCount(total).
|
||||
WithLink(r.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
||||
WithPayload(result)
|
||||
}
|
||||
|
||||
func (r *replicationAPI) GetReplicationPolicy(ctx context.Context, params operation.GetReplicationPolicyParams) middleware.Responder {
|
||||
if err := r.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceReplicationPolicy); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
policy, err := r.ctl.GetPolicy(ctx, params.ID)
|
||||
if err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewGetReplicationPolicyOK().WithPayload(convertReplicationPolicy(policy))
|
||||
}
|
||||
|
||||
func (r *replicationAPI) DeleteReplicationPolicy(ctx context.Context, params operation.DeleteReplicationPolicyParams) middleware.Responder {
|
||||
if err := r.RequireSystemAccess(ctx, rbac.ActionDelete, rbac.ResourceReplicationPolicy); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
if err := r.ctl.DeletePolicy(ctx, params.ID); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
return operation.NewDeleteReplicationPolicyOK()
|
||||
}
|
||||
|
||||
func (r *replicationAPI) StartReplication(ctx context.Context, params operation.StartReplicationParams) middleware.Responder {
|
||||
if err := r.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceReplication); err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
policy, err := r.ctl.GetPolicy(ctx, params.Execution.PolicyID)
|
||||
if err != nil {
|
||||
return r.SendError(ctx, err)
|
||||
}
|
||||
// the legacy replication scheduler job("src/jobservice/job/impl/replication/scheduler.go") calls the start replication API
|
||||
// to trigger the scheduled replication, a query string "trigger" is added when sending the request
|
||||
// here is the logic to cover this part
|
||||
@ -245,6 +384,73 @@ func (r *replicationAPI) GetReplicationLog(ctx context.Context, params operation
|
||||
}
|
||||
return operation.NewGetReplicationLogOK().WithContentType("text/plain").WithPayload(string(log))
|
||||
}
|
||||
|
||||
func convertReplicationPolicy(policy *rep.Policy) *models.ReplicationPolicy {
|
||||
p := &models.ReplicationPolicy{
|
||||
CreationTime: strfmt.DateTime(policy.CreationTime),
|
||||
Deletion: policy.ReplicateDeletion,
|
||||
Description: policy.Description,
|
||||
DestNamespace: policy.DestNamespace,
|
||||
Enabled: policy.Enabled,
|
||||
ID: policy.ID,
|
||||
Name: policy.Name,
|
||||
Override: policy.Override,
|
||||
ReplicateDeletion: policy.ReplicateDeletion,
|
||||
UpdateTime: strfmt.DateTime(policy.UpdateTime),
|
||||
}
|
||||
if policy.SrcRegistry != nil {
|
||||
p.SrcRegistry = convertRegistry(policy.SrcRegistry)
|
||||
}
|
||||
if policy.DestRegistry != nil {
|
||||
p.DestRegistry = convertRegistry(policy.DestRegistry)
|
||||
}
|
||||
if len(policy.Filters) > 0 {
|
||||
for _, filter := range policy.Filters {
|
||||
p.Filters = append(p.Filters, &models.ReplicationFilter{
|
||||
Type: string(filter.Type),
|
||||
Value: filter.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if policy.Trigger != nil {
|
||||
trigger := &models.ReplicationTrigger{
|
||||
Type: string(policy.Trigger.Type),
|
||||
}
|
||||
if policy.Trigger.Settings != nil {
|
||||
trigger.TriggerSettings = &models.ReplicationTriggerSettings{
|
||||
Cron: policy.Trigger.Settings.Cron,
|
||||
}
|
||||
}
|
||||
p.Trigger = trigger
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func convertRegistry(registry *model.Registry) *models.Registry {
|
||||
r := &models.Registry{
|
||||
CreationTime: strfmt.DateTime(registry.CreationTime),
|
||||
Description: registry.Description,
|
||||
ID: registry.ID,
|
||||
Insecure: registry.Insecure,
|
||||
Name: registry.Name,
|
||||
Status: registry.Status,
|
||||
Type: string(registry.Type),
|
||||
UpdateTime: strfmt.DateTime(registry.UpdateTime),
|
||||
URL: registry.URL,
|
||||
}
|
||||
if registry.Credential != nil {
|
||||
credential := &models.RegistryCredential{
|
||||
AccessKey: registry.Credential.AccessKey,
|
||||
Type: string(registry.Credential.Type),
|
||||
}
|
||||
if len(registry.Credential.AccessSecret) > 0 {
|
||||
credential.AccessSecret = "*****"
|
||||
}
|
||||
r.Credential = credential
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func convertExecution(execution *replication.Execution) *models.ReplicationExecution {
|
||||
exec := &models.ReplicationExecution{
|
||||
ID: execution.ID,
|
||||
|
@ -40,8 +40,6 @@ func registerLegacyRoutes() {
|
||||
|
||||
beego.Router("/api/"+version+"/replication/adapters", &api.ReplicationAdapterAPI{}, "get:List")
|
||||
beego.Router("/api/"+version+"/replication/adapterinfos", &api.ReplicationAdapterAPI{}, "get:ListAdapterInfos")
|
||||
beego.Router("/api/"+version+"/replication/policies", &api.ReplicationPolicyAPI{}, "get:List;post:Create")
|
||||
beego.Router("/api/"+version+"/replication/policies/:id([0-9]+)", &api.ReplicationPolicyAPI{}, "get:Get;put:Update;delete:Delete")
|
||||
|
||||
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/policies", &api.NotificationPolicyAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/policies/:id([0-9]+)", &api.NotificationPolicyAPI{})
|
||||
|
@ -5,12 +5,14 @@ package replication
|
||||
import (
|
||||
context "context"
|
||||
|
||||
model "github.com/goharbor/harbor/src/replication/model"
|
||||
controllerreplication "github.com/goharbor/harbor/src/controller/replication"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/goharbor/harbor/src/replication/model"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
|
||||
replication "github.com/goharbor/harbor/src/controller/replication"
|
||||
replication "github.com/goharbor/harbor/src/pkg/replication"
|
||||
)
|
||||
|
||||
// Controller is an autogenerated mock type for the Controller type
|
||||
@ -18,6 +20,41 @@ type Controller struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreatePolicy provides a mock function with given fields: ctx, policy
|
||||
func (_m *Controller) CreatePolicy(ctx context.Context, policy *replication.Policy) (int64, error) {
|
||||
ret := _m.Called(ctx, policy)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *replication.Policy) int64); ok {
|
||||
r0 = rf(ctx, policy)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *replication.Policy) error); ok {
|
||||
r1 = rf(ctx, policy)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DeletePolicy provides a mock function with given fields: ctx, id
|
||||
func (_m *Controller) DeletePolicy(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ExecutionCount provides a mock function with given fields: ctx, query
|
||||
func (_m *Controller) ExecutionCount(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
@ -40,15 +77,15 @@ func (_m *Controller) ExecutionCount(ctx context.Context, query *q.Query) (int64
|
||||
}
|
||||
|
||||
// GetExecution provides a mock function with given fields: ctx, executionID
|
||||
func (_m *Controller) GetExecution(ctx context.Context, executionID int64) (*replication.Execution, error) {
|
||||
func (_m *Controller) GetExecution(ctx context.Context, executionID int64) (*controllerreplication.Execution, error) {
|
||||
ret := _m.Called(ctx, executionID)
|
||||
|
||||
var r0 *replication.Execution
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *replication.Execution); ok {
|
||||
var r0 *controllerreplication.Execution
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *controllerreplication.Execution); ok {
|
||||
r0 = rf(ctx, executionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*replication.Execution)
|
||||
r0 = ret.Get(0).(*controllerreplication.Execution)
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,16 +99,39 @@ func (_m *Controller) GetExecution(ctx context.Context, executionID int64) (*rep
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetPolicy provides a mock function with given fields: ctx, id
|
||||
func (_m *Controller) GetPolicy(ctx context.Context, id int64) (*replication.Policy, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 *replication.Policy
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *replication.Policy); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*replication.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTask provides a mock function with given fields: ctx, taskID
|
||||
func (_m *Controller) GetTask(ctx context.Context, taskID int64) (*replication.Task, error) {
|
||||
func (_m *Controller) GetTask(ctx context.Context, taskID int64) (*controllerreplication.Task, error) {
|
||||
ret := _m.Called(ctx, taskID)
|
||||
|
||||
var r0 *replication.Task
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *replication.Task); ok {
|
||||
var r0 *controllerreplication.Task
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *controllerreplication.Task); ok {
|
||||
r0 = rf(ctx, taskID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*replication.Task)
|
||||
r0 = ret.Get(0).(*controllerreplication.Task)
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,15 +169,38 @@ func (_m *Controller) GetTaskLog(ctx context.Context, taskID int64) ([]byte, err
|
||||
}
|
||||
|
||||
// ListExecutions provides a mock function with given fields: ctx, query
|
||||
func (_m *Controller) ListExecutions(ctx context.Context, query *q.Query) ([]*replication.Execution, error) {
|
||||
func (_m *Controller) ListExecutions(ctx context.Context, query *q.Query) ([]*controllerreplication.Execution, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*replication.Execution
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*replication.Execution); ok {
|
||||
var r0 []*controllerreplication.Execution
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*controllerreplication.Execution); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*replication.Execution)
|
||||
r0 = ret.Get(0).([]*controllerreplication.Execution)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListPolicies provides a mock function with given fields: ctx, query
|
||||
func (_m *Controller) ListPolicies(ctx context.Context, query *q.Query) ([]*replication.Policy, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*replication.Policy
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*replication.Policy); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*replication.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,15 +215,15 @@ func (_m *Controller) ListExecutions(ctx context.Context, query *q.Query) ([]*re
|
||||
}
|
||||
|
||||
// ListTasks provides a mock function with given fields: ctx, query
|
||||
func (_m *Controller) ListTasks(ctx context.Context, query *q.Query) ([]*replication.Task, error) {
|
||||
func (_m *Controller) ListTasks(ctx context.Context, query *q.Query) ([]*controllerreplication.Task, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*replication.Task
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*replication.Task); ok {
|
||||
var r0 []*controllerreplication.Task
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*controllerreplication.Task); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*replication.Task)
|
||||
r0 = ret.Get(0).([]*controllerreplication.Task)
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,19 +237,40 @@ func (_m *Controller) ListTasks(ctx context.Context, query *q.Query) ([]*replica
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PolicyCount provides a mock function with given fields: ctx, query
|
||||
func (_m *Controller) PolicyCount(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Start provides a mock function with given fields: ctx, policy, resource, trigger
|
||||
func (_m *Controller) Start(ctx context.Context, policy *model.Policy, resource *model.Resource, trigger string) (int64, error) {
|
||||
func (_m *Controller) Start(ctx context.Context, policy *replication.Policy, resource *model.Resource, trigger string) (int64, error) {
|
||||
ret := _m.Called(ctx, policy, resource, trigger)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *model.Policy, *model.Resource, string) int64); ok {
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *replication.Policy, *model.Resource, string) int64); ok {
|
||||
r0 = rf(ctx, policy, resource, trigger)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *model.Policy, *model.Resource, string) error); ok {
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *replication.Policy, *model.Resource, string) error); ok {
|
||||
r1 = rf(ctx, policy, resource, trigger)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
@ -209,3 +313,24 @@ func (_m *Controller) TaskCount(ctx context.Context, query *q.Query) (int64, err
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdatePolicy provides a mock function with given fields: ctx, policy, props
|
||||
func (_m *Controller) UpdatePolicy(ctx context.Context, policy *replication.Policy, props ...string) error {
|
||||
_va := make([]interface{}, len(props))
|
||||
for _i := range props {
|
||||
_va[_i] = props[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, policy)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *replication.Policy, ...string) error); ok {
|
||||
r0 = rf(ctx, policy, props...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
@ -37,3 +37,7 @@ package pkg
|
||||
//go:generate mockery --case snake --dir ../../pkg/ldap --name Manager --output ./ldap --outpkg ldap
|
||||
//go:generate mockery --case snake --dir ../../pkg/allowlist --name Manager --output ./allowlist --outpkg robot
|
||||
//go:generate mockery --case snake --dir ../../pkg/allowlist/dao --name DAO --output ./allowlist/dao --outpkg dao
|
||||
//go:generate mockery --case snake --dir ../../pkg/reg/dao --name DAO --output ./reg/dao --outpkg dao
|
||||
//go:generate mockery --case snake --dir ../../pkg/reg --name Manager --output ./reg --outpkg manager
|
||||
//go:generate mockery --case snake --dir ../../pkg/replication/dao --name DAO --output ./replication/dao --outpkg dao
|
||||
//go:generate mockery --case snake --dir ../../pkg/replication --name Manager --output ./replication --outpkg manager
|
||||
|
141
src/testing/pkg/reg/dao/dao.go
Normal file
141
src/testing/pkg/reg/dao/dao.go
Normal file
@ -0,0 +1,141 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
models "github.com/goharbor/harbor/src/replication/dao/models"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
)
|
||||
|
||||
// DAO is an autogenerated mock type for the DAO type
|
||||
type DAO struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: ctx, query
|
||||
func (_m *DAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, registry
|
||||
func (_m *DAO) Create(ctx context.Context, registry *models.Registry) (int64, error) {
|
||||
ret := _m.Called(ctx, registry)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Registry) int64); ok {
|
||||
r0 = rf(ctx, registry)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *models.Registry) error); ok {
|
||||
r1 = rf(ctx, registry)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *DAO) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id
|
||||
func (_m *DAO) Get(ctx context.Context, id int64) (*models.Registry, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 *models.Registry
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *models.Registry); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Registry)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*models.Registry, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*models.Registry
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*models.Registry); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*models.Registry)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, registry, props
|
||||
func (_m *DAO) Update(ctx context.Context, registry *models.Registry, props ...string) error {
|
||||
_va := make([]interface{}, len(props))
|
||||
for _i := range props {
|
||||
_va[_i] = props[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, registry)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Registry, ...string) error); ok {
|
||||
r0 = rf(ctx, registry, props...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
140
src/testing/pkg/reg/manager.go
Normal file
140
src/testing/pkg/reg/manager.go
Normal file
@ -0,0 +1,140 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
model "github.com/goharbor/harbor/src/replication/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
)
|
||||
|
||||
// Manager is an autogenerated mock type for the Manager type
|
||||
type Manager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: ctx, query
|
||||
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, registry
|
||||
func (_m *Manager) Create(ctx context.Context, registry *model.Registry) (int64, error) {
|
||||
ret := _m.Called(ctx, registry)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *model.Registry) int64); ok {
|
||||
r0 = rf(ctx, registry)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *model.Registry) error); ok {
|
||||
r1 = rf(ctx, registry)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *Manager) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id
|
||||
func (_m *Manager) Get(ctx context.Context, id int64) (*model.Registry, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 *model.Registry
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *model.Registry); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Registry)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*model.Registry, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*model.Registry
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.Registry); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.Registry)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, registry, props
|
||||
func (_m *Manager) Update(ctx context.Context, registry *model.Registry, props ...string) error {
|
||||
_va := make([]interface{}, len(props))
|
||||
for _i := range props {
|
||||
_va[_i] = props[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, registry)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *model.Registry, ...string) error); ok {
|
||||
r0 = rf(ctx, registry, props...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
140
src/testing/pkg/replication/dao/dao.go
Normal file
140
src/testing/pkg/replication/dao/dao.go
Normal file
@ -0,0 +1,140 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
dao "github.com/goharbor/harbor/src/pkg/replication/dao"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
)
|
||||
|
||||
// DAO is an autogenerated mock type for the DAO type
|
||||
type DAO struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: ctx, query
|
||||
func (_m *DAO) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, policy
|
||||
func (_m *DAO) Create(ctx context.Context, policy *dao.Policy) (int64, error) {
|
||||
ret := _m.Called(ctx, policy)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *dao.Policy) int64); ok {
|
||||
r0 = rf(ctx, policy)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *dao.Policy) error); ok {
|
||||
r1 = rf(ctx, policy)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *DAO) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id
|
||||
func (_m *DAO) Get(ctx context.Context, id int64) (*dao.Policy, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 *dao.Policy
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *dao.Policy); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*dao.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*dao.Policy, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*dao.Policy
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*dao.Policy); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*dao.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, policy, props
|
||||
func (_m *DAO) Update(ctx context.Context, policy *dao.Policy, props ...string) error {
|
||||
_va := make([]interface{}, len(props))
|
||||
for _i := range props {
|
||||
_va[_i] = props[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, policy)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *dao.Policy, ...string) error); ok {
|
||||
r0 = rf(ctx, policy, props...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
140
src/testing/pkg/replication/manager.go
Normal file
140
src/testing/pkg/replication/manager.go
Normal file
@ -0,0 +1,140 @@
|
||||
// Code generated by mockery v2.1.0. DO NOT EDIT.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
q "github.com/goharbor/harbor/src/lib/q"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
replication "github.com/goharbor/harbor/src/pkg/replication"
|
||||
)
|
||||
|
||||
// Manager is an autogenerated mock type for the Manager type
|
||||
type Manager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: ctx, query
|
||||
func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, policy
|
||||
func (_m *Manager) Create(ctx context.Context, policy *replication.Policy) (int64, error) {
|
||||
ret := _m.Called(ctx, policy)
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *replication.Policy) int64); ok {
|
||||
r0 = rf(ctx, policy)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *replication.Policy) error); ok {
|
||||
r1 = rf(ctx, policy)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *Manager) Delete(ctx context.Context, id int64) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id
|
||||
func (_m *Manager) Get(ctx context.Context, id int64) (*replication.Policy, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 *replication.Policy
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *replication.Policy); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*replication.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, query
|
||||
func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*replication.Policy, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
var r0 []*replication.Policy
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*replication.Policy); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*replication.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, policy, props
|
||||
func (_m *Manager) Update(ctx context.Context, policy *replication.Policy, props ...string) error {
|
||||
_va := make([]interface{}, len(props))
|
||||
for _i := range props {
|
||||
_va[_i] = props[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, policy)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *replication.Policy, ...string) error); ok {
|
||||
r0 = rf(ctx, policy, props...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
@ -81,6 +81,20 @@ func (_m *ExecutionManager) Delete(ctx context.Context, id int64) error {
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteByVendor provides a mock function with given fields: ctx, vendorType, vendorID
|
||||
func (_m *ExecutionManager) DeleteByVendor(ctx context.Context, vendorType string, vendorID int64) error {
|
||||
ret := _m.Called(ctx, vendorType, vendorID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok {
|
||||
r0 = rf(ctx, vendorType, vendorID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id
|
||||
func (_m *ExecutionManager) Get(ctx context.Context, id int64) (*task.Execution, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
@ -1,35 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import time
|
||||
import base
|
||||
import swagger_client
|
||||
import v2_swagger_client
|
||||
|
||||
class Replication(base.Base, object):
|
||||
def __init__(self):
|
||||
super(Replication,self).__init__(api_type = "replication")
|
||||
|
||||
def wait_until_jobs_finish(self, rule_id, retry=10, interval=5, **kwargs):
|
||||
Succeed = False
|
||||
for i in range(retry):
|
||||
Succeed = False
|
||||
jobs = self.get_replication_executions(rule_id, **kwargs)
|
||||
for job in jobs:
|
||||
if job.status == "Succeed":
|
||||
return
|
||||
if not Succeed:
|
||||
time.sleep(interval)
|
||||
if not Succeed:
|
||||
raise Exception("The jobs not Succeed")
|
||||
|
||||
def trigger_replication_executions(self, rule_id, expect_status_code = 201, **kwargs):
|
||||
_, status_code, _ = self._get_client(**kwargs).start_replication_with_http_info({"policy_id":rule_id})
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
|
||||
def get_replication_executions(self, rule_id, expect_status_code = 200, **kwargs):
|
||||
data, status_code, _ = self._get_client(**kwargs).list_replication_executions_with_http_info(policy_id=rule_id)
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
return data
|
||||
|
||||
class Replication(base.Base):
|
||||
def create_replication_policy(self, dest_registry=None, src_registry=None, name=None, description="",
|
||||
dest_namespace = "", filters=None, trigger=swagger_client.ReplicationTrigger(type="manual",trigger_settings=swagger_client.TriggerSettings(cron="")),
|
||||
deletion=False, override=True, enabled=True, expect_status_code = 201, **kwargs):
|
||||
dest_namespace = "", filters=None, trigger=v2_swagger_client.ReplicationTrigger(type="manual",trigger_settings=v2_swagger_client.ReplicationTriggerSettings(cron="")),
|
||||
deletion=False, override=True, enabled=True, expect_status_code = 201, **kwargs):
|
||||
if name is None:
|
||||
name = base._random_name("rule")
|
||||
if filters is None:
|
||||
filters = []
|
||||
|
||||
client = self._get_client(**kwargs)
|
||||
policy = swagger_client.ReplicationPolicy(name=name, description=description,dest_namespace=dest_namespace,
|
||||
dest_registry=dest_registry, src_registry=src_registry,filters=filters,
|
||||
trigger=trigger, deletion=deletion, override=override, enabled=enabled)
|
||||
_, status_code, header = client.replication_policies_post_with_http_info(policy)
|
||||
policy = v2_swagger_client.ReplicationPolicy(name=name, description=description,dest_namespace=dest_namespace,
|
||||
dest_registry=dest_registry, src_registry=src_registry,filters=filters,
|
||||
trigger=trigger, deletion=deletion, override=override, enabled=enabled)
|
||||
_, status_code, header = self._get_client(**kwargs).create_replication_policy_with_http_info(policy)
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
return base._get_id_from_header(header), name
|
||||
|
||||
def get_replication_rule(self, param = None, rule_id = None, expect_status_code = 200, **kwargs):
|
||||
client = self._get_client(**kwargs)
|
||||
if rule_id is None:
|
||||
if param is None:
|
||||
param = dict()
|
||||
data, status_code, _ = client.replication_policies_id_get_with_http_info(param)
|
||||
data, status_code, _ = self._get_client(**kwargs).get_replication_policy_with_http_info(param)
|
||||
else:
|
||||
data, status_code, _ = client.replication_policies_id_get_with_http_info(rule_id)
|
||||
data, status_code, _ = self._get_client(**kwargs).get_replication_policy_with_http_info(rule_id)
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
return data
|
||||
|
||||
@ -46,6 +68,6 @@ class Replication(base.Base):
|
||||
# raise Exception(r"Check replication rule trigger failed, expect <{}> actual <{}>.".format(expect_trigger, get_trigger))
|
||||
|
||||
def delete_replication_rule(self, rule_id, expect_status_code = 200, **kwargs):
|
||||
client = self._get_client(**kwargs)
|
||||
_, status_code, _ = client.replication_policies_id_delete_with_http_info(rule_id)
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
_, status_code, _ = self._get_client(**kwargs).delete_replication_policy_with_http_info(rule_id)
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
import base
|
||||
import v2_swagger_client
|
||||
from v2_swagger_client.rest import ApiException
|
||||
|
||||
class ReplicationV2(base.Base, object):
|
||||
def __init__(self):
|
||||
super(ReplicationV2,self).__init__(api_type = "replication")
|
||||
|
||||
def wait_until_jobs_finish(self, rule_id, retry=10, interval=5, **kwargs):
|
||||
Succeed = False
|
||||
for i in range(retry):
|
||||
Succeed = False
|
||||
jobs = self.get_replication_executions(rule_id, **kwargs)
|
||||
for job in jobs:
|
||||
if job.status == "Succeed":
|
||||
return
|
||||
if not Succeed:
|
||||
time.sleep(interval)
|
||||
if not Succeed:
|
||||
raise Exception("The jobs not Succeed")
|
||||
|
||||
def trigger_replication_executions(self, rule_id, expect_status_code = 201, **kwargs):
|
||||
_, status_code, _ = self._get_client(**kwargs).start_replication_with_http_info({"policy_id":rule_id})
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
|
||||
def get_replication_executions(self, rule_id, expect_status_code = 200, **kwargs):
|
||||
data, status_code, _ = self._get_client(**kwargs).list_replication_executions_with_http_info(policy_id=rule_id)
|
||||
base._assert_status_code(expect_status_code, status_code)
|
||||
return data
|
||||
|
@ -9,8 +9,8 @@ from library.replication import Replication
|
||||
from library.registry import Registry
|
||||
from library.artifact import Artifact
|
||||
from library.repository import Repository
|
||||
from library.replication_v2 import ReplicationV2
|
||||
import swagger_client
|
||||
import v2_swagger_client
|
||||
from testutils import DOCKER_USER, DOCKER_PWD
|
||||
|
||||
class TestProjects(unittest.TestCase):
|
||||
@ -19,7 +19,6 @@ class TestProjects(unittest.TestCase):
|
||||
self.project = Project()
|
||||
self.user = User()
|
||||
self.replication = Replication()
|
||||
self.replication_v2 = ReplicationV2()
|
||||
self.registry = Registry()
|
||||
self.artifact = Artifact()
|
||||
self.repo = Repository()
|
||||
@ -83,17 +82,17 @@ class TestProjects(unittest.TestCase):
|
||||
#4. Create a pull-based rule for this registry;
|
||||
TestProjects.rule_id, rule_name = self.replication.create_replication_policy(src_registry=swagger_client.Registry(id=int(TestProjects.registry_id)),
|
||||
dest_namespace=TestProjects.project_name,
|
||||
filters=[swagger_client.ReplicationFilter(type="name",value="library/"+self.image),swagger_client.ReplicationFilter(type="tag",value=self.tag)],
|
||||
filters=[v2_swagger_client.ReplicationFilter(type="name",value="library/"+self.image),v2_swagger_client.ReplicationFilter(type="tag",value=self.tag)],
|
||||
**ADMIN_CLIENT)
|
||||
|
||||
#5. Check rule should be exist;
|
||||
self.replication.check_replication_rule_should_exist(TestProjects.rule_id, rule_name, **ADMIN_CLIENT)
|
||||
|
||||
#6. Trigger the rule;
|
||||
self.replication_v2.trigger_replication_executions(TestProjects.rule_id, **ADMIN_CLIENT)
|
||||
self.replication.trigger_replication_executions(TestProjects.rule_id, **ADMIN_CLIENT)
|
||||
|
||||
#7. Wait for completion of this replication job;
|
||||
self.replication_v2.wait_until_jobs_finish(TestProjects.rule_id,interval=30, **ADMIN_CLIENT)
|
||||
self.replication.wait_until_jobs_finish(TestProjects.rule_id,interval=30, **ADMIN_CLIENT)
|
||||
|
||||
#8. Check image is replicated into target project successfully.
|
||||
artifact = self.artifact.get_reference_info(TestProjects.project_name, self.image, self.tag, **ADMIN_CLIENT)
|
||||
|
@ -159,4 +159,4 @@ Test Case - Metrics
|
||||
|
||||
Test Case - Project Level Policy Content Trust
|
||||
[Tags] content_trust
|
||||
Harbor API Test ./tests/apitests/python/test_project_level_policy_content_trust.py
|
||||
Harbor API Test ./tests/apitests/python/test_project_level_policy_content_trust.py
|
Loading…
Reference in New Issue
Block a user