Refeactor replication policy APIs

Refeactor replication policy APIs

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2021-03-03 11:45:33 +08:00
parent 7694c131a4
commit 3d7fd070c7
62 changed files with 3322 additions and 3475 deletions

View File

@ -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. description: The auth mode of the system is not "oidc_auth", or the user is not onboarded via OIDC AuthN.
'500': '500':
description: Unexpected internal errors. 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: /labels:
get: get:
summary: List labels according to the query strings. summary: List labels according to the query strings.
@ -2205,73 +2039,6 @@ definitions:
type: integer type: integer
format: int32 format: int32
description: 'The count of the total repositories, only be seen when the user is admin.' 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: RegistryCredential:
type: object type: object
properties: properties:

View File

@ -2255,6 +2255,152 @@ paths:
$ref: '#/responses/404' $ref: '#/responses/404'
'500': '500':
$ref: '#/responses/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: /replication/executions:
get: get:
summary: List replication executions summary: List replication executions
@ -4407,6 +4553,78 @@ definitions:
cve_id: cve_id:
type: string type: string
description: The ID of the CVE, such as "CVE-2019-10164" 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: RegistryCredential:
type: object type: object
properties: properties:
@ -4426,6 +4644,7 @@ definitions:
type: integer type: integer
format: int64 format: int64
description: The registry ID. description: The registry ID.
x-omitempty: false
url: url:
type: string type: string
description: The registry URL string. description: The registry URL string.
@ -4448,9 +4667,11 @@ definitions:
description: Health status of the registry. description: Health status of the registry.
creation_time: creation_time:
type: string type: string
format: date-time
description: The create time of the policy. description: The create time of the policy.
update_time: update_time:
type: string type: string
format: date-time
description: The update time of the policy. description: The update time of the policy.
ResourceList: ResourceList:
type: object type: object

View File

@ -85,7 +85,7 @@ func constructReplicationPayload(event *event.ReplicationEvent) (*model.Payload,
return nil, nil, err return nil, nil, err
} }
rpPolicy, err := rep.PolicyCtl.Get(execution.PolicyID) rpPolicy, err := replication.Ctl.GetPolicy(ctx, execution.PolicyID)
if err != nil { if err != nil {
log.Errorf("failed to get replication policy %d: error: %v", execution.PolicyID, err) log.Errorf("failed to get replication policy %d: error: %v", execution.PolicyID, err)
return nil, nil, err return nil, nil, err

View File

@ -22,10 +22,11 @@ import (
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/event" "github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/controller/project" "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/core/config"
"github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/notification" "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"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
projecttesting "github.com/goharbor/harbor/src/testing/controller/project" projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
@ -38,9 +39,6 @@ import (
type fakedNotificationPolicyMgr struct { type fakedNotificationPolicyMgr struct {
} }
type fakedReplicationPolicyMgr struct {
}
type fakedReplicationRegistryMgr struct { type fakedReplicationRegistryMgr struct {
} }
@ -87,44 +85,6 @@ func (f *fakedNotificationPolicyMgr) GetRelatedPolices(int64, string) ([]*models
}, nil }, 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 // Add new registry
func (f *fakedReplicationRegistryMgr) Add(*model.Registry) (int64, error) { func (f *fakedReplicationRegistryMgr) Add(*model.Registry) (int64, error) {
return 0, nil return 0, nil
@ -171,27 +131,25 @@ func TestReplicationHandler_Handle(t *testing.T) {
config.Init() config.Init()
PolicyMgr := notification.PolicyMgr PolicyMgr := notification.PolicyMgr
rpPolicy := replication.PolicyCtl
rpRegistry := replication.RegistryMgr rpRegistry := replication.RegistryMgr
prj := project.Ctl prj := project.Ctl
repCtl := rep.Ctl repCtl := repctl.Ctl
defer func() { defer func() {
notification.PolicyMgr = PolicyMgr notification.PolicyMgr = PolicyMgr
replication.PolicyCtl = rpPolicy
replication.RegistryMgr = rpRegistry replication.RegistryMgr = rpRegistry
project.Ctl = prj project.Ctl = prj
rep.Ctl = repCtl repctl.Ctl = repCtl
}() }()
notification.PolicyMgr = &fakedNotificationPolicyMgr{} notification.PolicyMgr = &fakedNotificationPolicyMgr{}
replication.PolicyCtl = &fakedReplicationPolicyMgr{}
replication.RegistryMgr = &fakedReplicationRegistryMgr{} replication.RegistryMgr = &fakedReplicationRegistryMgr{}
projectCtl := &projecttesting.Controller{} projectCtl := &projecttesting.Controller{}
project.Ctl = projectCtl project.Ctl = projectCtl
mockRepCtl := &replicationtesting.Controller{} mockRepCtl := &replicationtesting.Controller{}
rep.Ctl = mockRepCtl repctl.Ctl = mockRepCtl
mockRepCtl.On("GetTask", mock.Anything, mock.Anything).Return(&rep.Task{}, nil) mockRepCtl.On("GetPolicy", mock.Anything, mock.Anything).Return(&reppkg.Policy{ID: 1}, nil)
mockRepCtl.On("GetExecution", mock.Anything, mock.Anything).Return(&rep.Execution{}, 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) mock.OnAnything(projectCtl, "GetByName").Return(&models.Project{ProjectID: 1}, nil)

View File

@ -26,6 +26,9 @@ import (
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q" "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/pkg/task"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
) )
@ -35,10 +38,25 @@ func init() {
task.SetExecutionSweeperCount(job.Replication, 50) task.SetExecutionSweeperCount(job.Replication, 50)
} }
// Ctl is a global replication controller instance
var Ctl = NewController()
// Controller defines the operations related with replication // Controller defines the operations related with replication
type Controller interface { 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 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 the replication specified by the execution ID
Stop(ctx context.Context, executionID int64) (err error) Stop(ctx context.Context, executionID int64) (err error)
// ExecutionCount returns the total count of executions according to the query // 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) 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 // NewController creates a new instance of the replication controller
func NewController() Controller { func NewController() Controller {
return &controller{ return &controller{
repMgr: replication.Mgr,
execMgr: task.ExecMgr, execMgr: task.ExecMgr,
taskMgr: task.Mgr, taskMgr: task.Mgr,
regMgr: reg.Mgr,
scheduler: scheduler.Sched,
flowCtl: flow.NewController(), flowCtl: flow.NewController(),
ormCreator: orm.Crt, ormCreator: orm.Crt,
wp: lib.NewWorkerPool(1024), wp: lib.NewWorkerPool(1024),
@ -75,14 +90,17 @@ func NewController() Controller {
} }
type controller struct { type controller struct {
repMgr replication.Manager
execMgr task.ExecutionManager execMgr task.ExecutionManager
taskMgr task.Manager taskMgr task.Manager
regMgr reg.Manager
scheduler scheduler.Scheduler
flowCtl flow.Controller flowCtl flow.Controller
ormCreator orm.Creator ormCreator orm.Creator
wp *lib.WorkerPool 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) logger := log.GetLogger(ctx)
if !policy.Enabled { if !policy.Enabled {
return 0, errors.New(nil).WithCode(errors.PreconditionCode). return 0, errors.New(nil).WithCode(errors.PreconditionCode).

View File

@ -22,11 +22,14 @@ import (
"github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib" "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"
"github.com/goharbor/harbor/src/pkg/task/dao" "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/lib/orm"
"github.com/goharbor/harbor/src/testing/mock" "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" testingTask "github.com/goharbor/harbor/src/testing/pkg/task"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -34,18 +37,27 @@ import (
type replicationTestSuite struct { type replicationTestSuite struct {
suite.Suite suite.Suite
ctl *controller ctl *controller
repMgr *testingrep.Manager
regMgr *testingreg.Manager
execMgr *testingTask.ExecutionManager execMgr *testingTask.ExecutionManager
taskMgr *testingTask.Manager taskMgr *testingTask.Manager
scheduler *testingscheduler.Scheduler
flowCtl *flowController flowCtl *flowController
ormCreator *orm.Creator ormCreator *orm.Creator
} }
func (r *replicationTestSuite) SetupSuite() { func (r *replicationTestSuite) SetupTest() {
r.repMgr = &testingrep.Manager{}
r.regMgr = &testingreg.Manager{}
r.execMgr = &testingTask.ExecutionManager{} r.execMgr = &testingTask.ExecutionManager{}
r.taskMgr = &testingTask.Manager{} r.taskMgr = &testingTask.Manager{}
r.scheduler = &testingscheduler.Scheduler{}
r.flowCtl = &flowController{} r.flowCtl = &flowController{}
r.ormCreator = &orm.Creator{} r.ormCreator = &orm.Creator{}
r.ctl = &controller{ r.ctl = &controller{
repMgr: r.repMgr,
regMgr: r.regMgr,
scheduler: r.scheduler,
execMgr: r.execMgr, execMgr: r.execMgr,
taskMgr: r.taskMgr, taskMgr: r.taskMgr,
flowCtl: r.flowCtl, flowCtl: r.flowCtl,
@ -56,7 +68,7 @@ func (r *replicationTestSuite) SetupSuite() {
func (r *replicationTestSuite) TestStart() { func (r *replicationTestSuite) TestStart() {
// policy is disabled // 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) r.Require().NotNil(err)
// got error when running the replication flow // 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.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.flowCtl.On("Start", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("error"))
r.ormCreator.On("Create").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.Require().Nil(err)
r.Equal(int64(1), id) r.Equal(int64(1), id)
time.Sleep(1 * time.Second) // wait the functions called in the goroutine 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()) r.ormCreator.AssertExpectations(r.T())
// reset the mocks // reset the mocks
r.SetupSuite() r.SetupTest()
// got no error when running the replication flow // 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("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.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.flowCtl.On("Start", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
r.ormCreator.On("Create").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.Require().Nil(err)
r.Equal(int64(1), id) r.Equal(int64(1), id)
time.Sleep(1 * time.Second) // wait the functions called in the goroutine time.Sleep(1 * time.Second) // wait the functions called in the goroutine
@ -213,6 +225,11 @@ func (r *replicationTestSuite) TestGetTask() {
} }
func (r *replicationTestSuite) TestGetTaskLog() { 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) r.taskMgr.On("GetLog", mock.Anything, mock.Anything).Return([]byte{'a'}, nil)
data, err := r.ctl.GetTaskLog(nil, 1) data, err := r.ctl.GetTaskLog(nil, 1)
r.Require().Nil(err) r.Require().Nil(err)

View File

@ -16,6 +16,7 @@ package flow
import ( import (
"context" "context"
"github.com/goharbor/harbor/src/pkg/replication"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
) )
@ -27,7 +28,7 @@ type Flow interface {
// Controller controls the replication flow // Controller controls the replication flow
type Controller interface { 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 // NewController returns an instance of the default flow controller
@ -37,7 +38,7 @@ func NewController() Controller {
type controller struct{} 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 // deletion flow
if resource != nil && resource.Deleted { if resource != nil && resource.Deleted {
return NewDeletionFlow(executionID, policy, resource).Run(ctx) return NewDeletionFlow(executionID, policy, resource).Run(ctx)

View File

@ -17,6 +17,7 @@ package flow
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/goharbor/harbor/src/pkg/replication"
"github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
@ -27,7 +28,7 @@ import (
type copyFlow struct { type copyFlow struct {
executionID int64 executionID int64
resources []*model.Resource resources []*model.Resource
policy *model.Policy policy *replication.Policy
executionMgr task.ExecutionManager executionMgr task.ExecutionManager
taskMgr task.Manager taskMgr task.Manager
} }
@ -35,7 +36,7 @@ type copyFlow struct {
// NewCopyFlow returns an instance of the copy flow which replicates the resources from // 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, // the source registry to the destination registry. If the parameter "resources" isn't provided,
// will fetch the resources first // 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 &copyFlow{ return &copyFlow{
executionMgr: task.ExecMgr, executionMgr: task.ExecMgr,
taskMgr: task.Mgr, taskMgr: task.Mgr,

View File

@ -13,6 +13,7 @@ package flow
import ( import (
"context" "context"
"github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/replication"
"github.com/goharbor/harbor/src/replication/adapter" "github.com/goharbor/harbor/src/replication/adapter"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"testing" "testing"
@ -60,7 +61,7 @@ func (c *copyFlowTestSuite) TestRun() {
taskMgr := &testingTask.Manager{} taskMgr := &testingTask.Manager{}
taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
policy := &model.Policy{ policy := &replication.Policy{
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
Type: "TEST_FOR_COPY_FLOW", Type: "TEST_FOR_COPY_FLOW",
}, },

View File

@ -17,6 +17,7 @@ package flow
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/goharbor/harbor/src/pkg/replication"
"github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/task" "github.com/goharbor/harbor/src/pkg/task"
@ -25,7 +26,7 @@ import (
type deletionFlow struct { type deletionFlow struct {
executionID int64 executionID int64
policy *model.Policy policy *replication.Policy
executionMgr task.ExecutionManager executionMgr task.ExecutionManager
taskMgr task.Manager taskMgr task.Manager
resources []*model.Resource resources []*model.Resource
@ -33,7 +34,7 @@ type deletionFlow struct {
// NewDeletionFlow returns an instance of the delete flow which deletes the resources // NewDeletionFlow returns an instance of the delete flow which deletes the resources
// on the destination registry // 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{ return &deletionFlow{
executionMgr: task.ExecMgr, executionMgr: task.ExecMgr,
taskMgr: task.Mgr, taskMgr: task.Mgr,

View File

@ -16,6 +16,7 @@ package flow
import ( import (
"context" "context"
"github.com/goharbor/harbor/src/pkg/replication"
"testing" "testing"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
@ -32,7 +33,7 @@ func (d *deletionFlowTestSuite) TestRun() {
taskMgr := &task.Manager{} taskMgr := &task.Manager{}
taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
policy := &model.Policy{ policy := &replication.Policy{
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
Type: model.RegistryTypeHarbor, Type: model.RegistryTypeHarbor,
}, },

View File

@ -16,6 +16,7 @@ package flow
import ( import (
"fmt" "fmt"
"github.com/goharbor/harbor/src/pkg/replication"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
adp "github.com/goharbor/harbor/src/replication/adapter" 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 // 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 srcAdapter, dstAdapter adp.Adapter
var err error var err error
@ -52,7 +53,7 @@ func initialize(policy *model.Policy) (adp.Adapter, adp.Adapter, error) {
} }
// fetch resources from the source registry // 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 var resTypes []model.ResourceType
for _, filter := range policy.Filters { for _, filter := range policy.Filters {
if filter.Type == model.FilterTypeResource { 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 // assemble the source resources by filling the registry information
func assembleSourceResources(resources []*model.Resource, func assembleSourceResources(resources []*model.Resource,
policy *model.Policy) []*model.Resource { policy *replication.Policy) []*model.Resource {
for _, resource := range resources { for _, resource := range resources {
resource.Registry = policy.SrcRegistry 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 // assemble the destination resources by filling the metadata, registry and override properties
func assembleDestinationResources(resources []*model.Resource, func assembleDestinationResources(resources []*model.Resource,
policy *model.Policy) []*model.Resource { policy *replication.Policy) []*model.Resource {
var result []*model.Resource var result []*model.Resource
for _, resource := range resources { for _, resource := range resources {
res := &model.Resource{ res := &model.Resource{

View File

@ -15,6 +15,7 @@
package flow package flow
import ( import (
"github.com/goharbor/harbor/src/pkg/replication"
"testing" "testing"
"github.com/goharbor/harbor/src/replication/adapter" "github.com/goharbor/harbor/src/replication/adapter"
@ -35,7 +36,7 @@ func (s *stageTestSuite) TestInitialize() {
factory.On("AdapterPattern").Return(nil) factory.On("AdapterPattern").Return(nil)
adapter.RegisterFactory(model.RegistryTypeHarbor, factory) adapter.RegisterFactory(model.RegistryTypeHarbor, factory)
policy := &model.Policy{ policy := &replication.Policy{
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
Type: model.RegistryTypeHarbor, Type: model.RegistryTypeHarbor,
}, },
@ -60,7 +61,7 @@ func (s *stageTestSuite) TestFetchResources() {
{}, {},
{}, {},
}, nil) }, nil)
policy := &model.Policy{} policy := &replication.Policy{}
resources, err := fetchResources(adapter, policy) resources, err := fetchResources(adapter, policy)
s.Require().Nil(err) s.Require().Nil(err)
s.Len(resources, 2) s.Len(resources, 2)
@ -80,7 +81,7 @@ func (s *stageTestSuite) TestAssembleSourceResources() {
Override: false, Override: false,
}, },
} }
policy := &model.Policy{ policy := &replication.Policy{
SrcRegistry: &model.Registry{ SrcRegistry: &model.Registry{
ID: 1, ID: 1,
}, },
@ -103,7 +104,7 @@ func (s *stageTestSuite) TestAssembleDestinationResources() {
Override: false, Override: false,
}, },
} }
policy := &model.Policy{ policy := &replication.Policy{
DestRegistry: &model.Registry{}, DestRegistry: &model.Registry{},
DestNamespace: "test", DestNamespace: "test",
Override: true, Override: true,

View File

@ -8,6 +8,8 @@ import (
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
model "github.com/goharbor/harbor/src/replication/model" 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 // 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 // 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) ret := _m.Called(ctx, executionID, policy, resource)
var r0 error 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) r0 = rf(ctx, executionID, policy, resource)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)

View 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(),
}
}

View 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())
}

View File

@ -120,9 +120,6 @@ func init() {
beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List") 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", &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/:id([0-9]+)", &NotificationPolicyAPI{})
beego.Router("/api/projects/:pid([0-9]+)/webhook/policies/test", &NotificationPolicyAPI{}, "post:Test") beego.Router("/api/projects/:pid([0-9]+)/webhook/policies/test", &NotificationPolicyAPI{}, "post:Test")

View File

@ -11,23 +11,21 @@ import (
"strconv" "strconv"
"github.com/goharbor/harbor/src/common/utils" "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/core/api/models"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/replication" "github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/adapter" "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/model"
"github.com/goharbor/harbor/src/replication/policy"
"github.com/goharbor/harbor/src/replication/registry" "github.com/goharbor/harbor/src/replication/registry"
) )
// RegistryAPI handles requests to /api/registries/{}. It manages registries integrated to Harbor. // RegistryAPI handles requests to /api/registries/{}. It manages registries integrated to Harbor.
type RegistryAPI struct { type RegistryAPI struct {
BaseController BaseController
manager registry.Manager manager registry.Manager
policyCtl policy.Controller resource types.Resource
resource types.Resource
} }
// Prepare validates the user // Prepare validates the user
@ -40,7 +38,6 @@ func (t *RegistryAPI) Prepare() {
t.resource = system.NewNamespace().Resource(rbac.ResourceRegistry) t.resource = system.NewNamespace().Resource(rbac.ResourceRegistry)
t.manager = replication.RegistryMgr t.manager = replication.RegistryMgr
t.policyCtl = replication.PolicyCtl
} }
// Ping checks health status of a registry // 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. // Check whether there are replication policies that use this registry as source registry.
total, _, err := t.policyCtl.List([]*model.PolicyQuery{ total, err := rep.Ctl.PolicyCount(orm.Context(), &q.Query{
{ Keywords: map[string]interface{}{
SrcRegistry: id, "SrcRegistryID": id,
}, },
}...) })
if err != nil { if err != nil {
t.SendInternalServerError(fmt.Errorf("List replication policies with source registry %d error: %v", id, err)) t.SendInternalServerError(fmt.Errorf("List replication policies with source registry %d error: %v", id, err))
return return
@ -385,11 +382,11 @@ func (t *RegistryAPI) Delete() {
} }
// Check whether there are replication policies that use this registry as destination registry. // Check whether there are replication policies that use this registry as destination registry.
total, _, err = t.policyCtl.List([]*model.PolicyQuery{ total, err = rep.Ctl.PolicyCount(orm.Context(), &q.Query{
{ Keywords: map[string]interface{}{
DestRegistry: id, "DestRegistryID": id,
}, },
}...) })
if err != nil { if err != nil {
t.SendInternalServerError(fmt.Errorf("List replication policies with destination registry %d error: %v", id, err)) t.SendInternalServerError(fmt.Errorf("List replication policies with destination registry %d error: %v", id, err))
return return
@ -434,7 +431,7 @@ func (t *RegistryAPI) GetInfo() {
} }
var registry *model.Registry var registry *model.Registry
if id == 0 { if id == 0 {
registry = event.GetLocalRegistry() registry = rep.GetLocalRegistry()
} else { } else {
registry, err = t.manager.Get(id) registry, err = t.manager.Get(id)
if err != nil { if err != nil {

View File

@ -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
}

View File

@ -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
View 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(&registries); 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
View 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
View 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)
}

View 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{})
}

View 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
}

View File

@ -0,0 +1,157 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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{})
}

View File

@ -0,0 +1,48 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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"
}

View 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)
}

View 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{})
}

View 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)
}

View 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)
}

View File

@ -63,6 +63,9 @@ type ExecutionManager interface {
StopAndWait(ctx context.Context, id int64, timeout time.Duration) (err error) StopAndWait(ctx context.Context, id int64, timeout time.Duration) (err error)
// Delete the specified execution and its tasks // Delete the specified execution and its tasks
Delete(ctx context.Context, id int64) (err error) 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 the specified execution
Get(ctx context.Context, id int64) (execution *Execution, err error) Get(ctx context.Context, id int64) (execution *Execution, err error)
// List executions according to the query // 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) 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) { func (e *executionManager) Get(ctx context.Context, id int64) (*Execution, error) {
execution, err := e.executionDAO.Get(ctx, id) execution, err := e.executionDAO.Get(ctx, id)
if err != nil { if err != nil {

View File

@ -6,6 +6,5 @@ import (
func init() { func init() {
orm.RegisterModel( orm.RegisterModel(
new(Registry), new(Registry))
new(RepPolicy))
} }

View File

@ -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"
}

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -18,15 +18,13 @@ import (
"errors" "errors"
"fmt" "fmt"
commonthttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/controller/replication" "github.com/goharbor/harbor/src/controller/replication"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm" "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/pkg/task"
"github.com/goharbor/harbor/src/replication/config"
"github.com/goharbor/harbor/src/replication/filter" "github.com/goharbor/harbor/src/replication/filter"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
"github.com/goharbor/harbor/src/replication/policy"
"github.com/goharbor/harbor/src/replication/registry" "github.com/goharbor/harbor/src/replication/registry"
) )
@ -36,16 +34,14 @@ type Handler interface {
} }
// NewHandler ... // NewHandler ...
func NewHandler(policyCtl policy.Controller, registryMgr registry.Manager) Handler { func NewHandler(registryMgr registry.Manager) Handler {
return &handler{ return &handler{
policyCtl: policyCtl,
registryMgr: registryMgr, registryMgr: registryMgr,
ctl: replication.Ctl, ctl: replication.Ctl,
} }
} }
type handler struct { type handler struct {
policyCtl policy.Controller
registryMgr registry.Manager registryMgr registry.Manager
ctl replication.Controller ctl replication.Controller
} }
@ -56,7 +52,7 @@ func (h *handler) Handle(event *Event) error {
len(event.Resource.Metadata.Artifacts) == 0 { len(event.Resource.Metadata.Artifacts) == 0 {
return errors.New("invalid event") return errors.New("invalid event")
} }
var policies []*model.Policy var policies []*rep.Policy
var err error var err error
switch event.Type { switch event.Type {
case EventTypeArtifactPush, EventTypeChartUpload, EventTypeTagDelete, case EventTypeArtifactPush, EventTypeChartUpload, EventTypeTagDelete,
@ -75,9 +71,6 @@ func (h *handler) Handle(event *Event) error {
} }
for _, policy := range policies { 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) id, err := h.ctl.Start(orm.Context(), policy, event.Resource, task.ExecutionTriggerEvent)
if err != nil { if err != nil {
return err return err
@ -87,12 +80,12 @@ func (h *handler) Handle(event *Event) error {
return nil return nil
} }
func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*model.Policy, error) { func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*rep.Policy, error) {
_, policies, err := h.policyCtl.List() policies, err := replication.Ctl.ListPolicies(orm.Context(), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := []*model.Policy{} result := []*rep.Policy{}
for _, policy := range policies { for _, policy := range policies {
// disabled // disabled
if !policy.Enabled { if !policy.Enabled {
@ -112,7 +105,7 @@ func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*model.Policy,
continue continue
} }
// doesn't replicate deletion // doesn't replicate deletion
if resource.Deleted && !policy.Deletion { if resource.Deleted && !policy.ReplicateDeletion {
continue continue
} }
@ -129,52 +122,3 @@ func (h *handler) getRelatedPolicies(resource *model.Resource) ([]*model.Policy,
} }
return result, nil 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(),
}
}

View File

@ -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)
}

View File

@ -14,13 +14,6 @@
package model package model
import (
"fmt"
"github.com/astaxie/beego/validation"
"github.com/robfig/cron"
"time"
)
// const definition // const definition
const ( const (
FilterTypeResource FilterType = "resource" FilterTypeResource FilterType = "resource"
@ -33,109 +26,6 @@ const (
TriggerTypeEventBased TriggerType = "event_based" 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. // FilterType represents the type info of the filter.
type FilterType string type FilterType string
@ -158,15 +48,3 @@ type Trigger struct {
type TriggerSettings struct { type TriggerSettings struct {
Cron string `json:"cron"` 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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -69,7 +69,7 @@ func (m *DefaultManager) Get(id int64) (*model.Registry, error) {
return nil, nil return nil, nil
} }
return fromDaoModel(registry) return FromDaoModel(registry)
} }
// GetByName gets a registry by its name // GetByName gets a registry by its name
@ -83,7 +83,7 @@ func (m *DefaultManager) GetByName(name string) (*model.Registry, error) {
return nil, nil return nil, nil
} }
return fromDaoModel(registry) return FromDaoModel(registry)
} }
// List lists registries according to query provided. // 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 var results []*model.Registry
for _, r := range registries { for _, r := range registries {
registry, err := fromDaoModel(r) registry, err := FromDaoModel(r)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
@ -107,7 +107,7 @@ func (m *DefaultManager) List(query *q.Query) (int64, []*model.Registry, error)
// Add adds a new registry // Add adds a new registry
func (m *DefaultManager) Add(registry *model.Registry) (int64, error) { func (m *DefaultManager) Add(registry *model.Registry) (int64, error) {
r, err := toDaoModel(registry) r, err := ToDaoModel(registry)
if err != nil { if err != nil {
log.Errorf("Convert registry model to dao layer model error: %v", err) log.Errorf("Convert registry model to dao layer model error: %v", err)
return -1, err return -1, err
@ -124,7 +124,7 @@ func (m *DefaultManager) Add(registry *model.Registry) (int64, error) {
// Update updates a registry // Update updates a registry
func (m *DefaultManager) Update(registry *model.Registry, props ...string) error { func (m *DefaultManager) Update(registry *model.Registry, props ...string) error {
r, err := toDaoModel(registry) r, err := ToDaoModel(registry)
if err != nil { if err != nil {
log.Errorf("Convert registry model to dao layer model error: %v", err) log.Errorf("Convert registry model to dao layer model error: %v", err)
return err return err
@ -219,9 +219,9 @@ func encrypt(secret string) (string, error) {
return encrypted, nil 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. // 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{ r := &model.Registry{
ID: registry.ID, ID: registry.ID,
Name: registry.Name, Name: registry.Name,
@ -254,9 +254,9 @@ func fromDaoModel(registry *models.Registry) (*model.Registry, error) {
return r, nil 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. // 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{ m := &models.Registry{
ID: registry.ID, ID: registry.ID,
URL: registry.URL, URL: registry.URL,

View File

@ -15,20 +15,12 @@
package replication package replication
import ( import (
"context"
"fmt"
"strconv"
"time" "time"
"github.com/goharbor/harbor/src/controller/replication"
cfg "github.com/goharbor/harbor/src/core/config" cfg "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/log" "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/config"
"github.com/goharbor/harbor/src/replication/event" "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" "github.com/goharbor/harbor/src/replication/registry"
// register the Harbor adapter // register the Harbor adapter
@ -66,41 +58,12 @@ import (
) )
var ( var (
// PolicyCtl is a global policy controller
PolicyCtl policy.Controller
// RegistryMgr is a global registry manager // RegistryMgr is a global registry manager
RegistryMgr registry.Manager RegistryMgr registry.Manager
// EventHandler handles images/chart pull/push events // EventHandler handles images/chart pull/push events
EventHandler event.Handler 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 // Init the global variables and configurations
func Init(closing, done chan struct{}) error { func Init(closing, done chan struct{}) error {
// init config // init config
@ -116,10 +79,8 @@ func Init(closing, done chan struct{}) error {
} }
// init registry manager // init registry manager
RegistryMgr = registry.NewDefaultManager() RegistryMgr = registry.NewDefaultManager()
// init policy controller
PolicyCtl = controller.NewController()
// init event handler // init event handler
EventHandler = event.NewHandler(PolicyCtl, RegistryMgr) EventHandler = event.NewHandler(RegistryMgr)
log.Debug("the replication initialization completed") log.Debug("the replication initialization completed")
// Start health checker for registries // Start health checker for registries

View File

@ -36,7 +36,6 @@ func TestInit(t *testing.T) {
config.InitWithSettings(nil) config.InitWithSettings(nil)
err = Init(make(chan struct{}), make(chan struct{})) err = Init(make(chan struct{}), make(chan struct{}))
require.Nil(t, err) require.Nil(t, err)
assert.NotNil(t, PolicyCtl)
assert.NotNil(t, RegistryMgr) assert.NotNil(t, RegistryMgr)
assert.NotNil(t, EventHandler) assert.NotNil(t, EventHandler)
} }

View File

@ -56,14 +56,22 @@ func (*BaseAPI) SendError(ctx context.Context, err error) middleware.Responder {
return NewErrResponder(err) return NewErrResponder(err)
} }
// HasPermission returns true when the request has action permission on resource // GetSecurityContext from the provided context
func (*BaseAPI) HasPermission(ctx context.Context, action rbac.Action, resource rbac.Resource) bool { func (*BaseAPI) GetSecurityContext(ctx context.Context) (security.Context, error) {
s, ok := security.FromContext(ctx) sc, ok := security.FromContext(ctx)
if !ok { 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 false
} }
return s.Can(ctx, action, resource) 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...) { if b.HasProjectPermission(ctx, projectIDOrName, action, subresource...) {
return nil return nil
} }
secCtx, ok := security.FromContext(ctx) secCtx, err := b.GetSecurityContext(ctx)
if !ok { if err != nil {
return errors.UnauthorizedError(errors.New("security context not found")) return err
} }
if !secCtx.IsAuthenticated() { if !secCtx.IsAuthenticated() {
return errors.UnauthorizedError(nil) 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 // 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 { func (b *BaseAPI) RequireSystemAccess(ctx context.Context, action rbac.Action, subresource ...rbac.Resource) error {
secCtx, ok := security.FromContext(ctx) secCtx, err := b.GetSecurityContext(ctx)
if !ok { if err != nil {
return errors.UnauthorizedError(errors.New("security context not found")) return err
} }
if !secCtx.IsAuthenticated() { if !secCtx.IsAuthenticated() {
return errors.UnauthorizedError(nil) 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 // RequireAuthenticated checks it's authenticated according to the security context
func (b *BaseAPI) RequireAuthenticated(ctx context.Context) error { func (b *BaseAPI) RequireAuthenticated(ctx context.Context) error {
secCtx, ok := security.FromContext(ctx) secCtx, err := b.GetSecurityContext(ctx)
if !ok { if err != nil {
return errors.UnauthorizedError(errors.New("security context not found")) return err
} }
if !secCtx.IsAuthenticated() { if !secCtx.IsAuthenticated() {
return errors.UnauthorizedError(nil) return errors.UnauthorizedError(nil)

View File

@ -16,59 +16,198 @@ package handler
import ( import (
"context" "context"
"github.com/goharbor/harbor/src/common/rbac" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/replication" "github.com/goharbor/harbor/src/controller/replication"
"github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/lib/q"
rep "github.com/goharbor/harbor/src/pkg/replication"
"github.com/goharbor/harbor/src/pkg/task" "github.com/goharbor/harbor/src/pkg/task"
replica "github.com/goharbor/harbor/src/replication" "github.com/goharbor/harbor/src/replication/model"
"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/server/v2.0/models" "github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/replication" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/replication"
) )
func newReplicationAPI() *replicationAPI { func newReplicationAPI() *replicationAPI {
return &replicationAPI{ return &replicationAPI{
ctl: replication.Ctl, ctl: replication.Ctl,
policyMgr: manager.NewDefaultManager(),
} }
} }
type replicationAPI struct { type replicationAPI struct {
BaseAPI BaseAPI
ctl replication.Controller ctl replication.Controller
policyMgr policy.Controller
} }
func (r *replicationAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder { func (r *replicationAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
return nil return nil
} }
func (r *replicationAPI) StartReplication(ctx context.Context, params operation.StartReplicationParams) middleware.Responder { func (r *replicationAPI) CreateReplicationPolicy(ctx context.Context, params operation.CreateReplicationPolicyParams) 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.ResourceReplicationPolicy); err != nil {
if err := r.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceReplication); err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }
policy, err := r.policyMgr.Get(params.Execution.PolicyID) sc, err := r.GetSecurityContext(ctx)
if err != nil { if err != nil {
return r.SendError(ctx, err) return r.SendError(ctx, err)
} }
if policy == nil { policy := &rep.Policy{
return r.SendError(ctx, errors.New(nil).WithCode(errors.NotFoundCode). Name: params.Policy.Name,
WithMessage("the replication policy %d not found", params.Execution.PolicyID)) 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) 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 // 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 // to trigger the scheduled replication, a query string "trigger" is added when sending the request
// here is the logic to cover this part // 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)) 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 { func convertExecution(execution *replication.Execution) *models.ReplicationExecution {
exec := &models.ReplicationExecution{ exec := &models.ReplicationExecution{
ID: execution.ID, ID: execution.ID,

View File

@ -40,8 +40,6 @@ func registerLegacyRoutes() {
beego.Router("/api/"+version+"/replication/adapters", &api.ReplicationAdapterAPI{}, "get:List") 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/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", &api.NotificationPolicyAPI{}, "get:List;post:Post")
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/policies/:id([0-9]+)", &api.NotificationPolicyAPI{}) beego.Router("/api/"+version+"/projects/:pid([0-9]+)/webhook/policies/:id([0-9]+)", &api.NotificationPolicyAPI{})

View File

@ -5,12 +5,14 @@ package replication
import ( import (
context "context" context "context"
model "github.com/goharbor/harbor/src/replication/model" controllerreplication "github.com/goharbor/harbor/src/controller/replication"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
model "github.com/goharbor/harbor/src/replication/model"
q "github.com/goharbor/harbor/src/lib/q" 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 // Controller is an autogenerated mock type for the Controller type
@ -18,6 +20,41 @@ type Controller struct {
mock.Mock 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 // ExecutionCount provides a mock function with given fields: ctx, query
func (_m *Controller) ExecutionCount(ctx context.Context, query *q.Query) (int64, error) { func (_m *Controller) ExecutionCount(ctx context.Context, query *q.Query) (int64, error) {
ret := _m.Called(ctx, query) 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 // 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) ret := _m.Called(ctx, executionID)
var r0 *replication.Execution var r0 *controllerreplication.Execution
if rf, ok := ret.Get(0).(func(context.Context, int64) *replication.Execution); ok { if rf, ok := ret.Get(0).(func(context.Context, int64) *controllerreplication.Execution); ok {
r0 = rf(ctx, executionID) r0 = rf(ctx, executionID)
} else { } else {
if ret.Get(0) != nil { 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 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 // 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) ret := _m.Called(ctx, taskID)
var r0 *replication.Task var r0 *controllerreplication.Task
if rf, ok := ret.Get(0).(func(context.Context, int64) *replication.Task); ok { if rf, ok := ret.Get(0).(func(context.Context, int64) *controllerreplication.Task); ok {
r0 = rf(ctx, taskID) r0 = rf(ctx, taskID)
} else { } else {
if ret.Get(0) != nil { 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 // 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) ret := _m.Called(ctx, query)
var r0 []*replication.Execution var r0 []*controllerreplication.Execution
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*replication.Execution); ok { if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*controllerreplication.Execution); ok {
r0 = rf(ctx, query) r0 = rf(ctx, query)
} else { } else {
if ret.Get(0) != nil { 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 // 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) ret := _m.Called(ctx, query)
var r0 []*replication.Task var r0 []*controllerreplication.Task
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*replication.Task); ok { if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*controllerreplication.Task); ok {
r0 = rf(ctx, query) r0 = rf(ctx, query)
} else { } else {
if ret.Get(0) != nil { 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 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 // 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) ret := _m.Called(ctx, policy, resource, trigger)
var r0 int64 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) r0 = rf(ctx, policy, resource, trigger)
} else { } else {
r0 = ret.Get(0).(int64) r0 = ret.Get(0).(int64)
} }
var r1 error 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) r1 = rf(ctx, policy, resource, trigger)
} else { } else {
r1 = ret.Error(1) r1 = ret.Error(1)
@ -209,3 +313,24 @@ func (_m *Controller) TaskCount(ctx context.Context, query *q.Query) (int64, err
return r0, r1 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
}

View File

@ -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/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 --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/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

View 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
}

View 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
}

View 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
}

View 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
}

View File

@ -81,6 +81,20 @@ func (_m *ExecutionManager) Delete(ctx context.Context, id int64) error {
return r0 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 // Get provides a mock function with given fields: ctx, id
func (_m *ExecutionManager) Get(ctx context.Context, id int64) (*task.Execution, error) { func (_m *ExecutionManager) Get(ctx context.Context, id int64) (*task.Execution, error) {
ret := _m.Called(ctx, id) ret := _m.Called(ctx, id)

View File

@ -1,35 +1,57 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
import time import time
import base 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="", 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="")), 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): deletion=False, override=True, enabled=True, expect_status_code = 201, **kwargs):
if name is None: if name is None:
name = base._random_name("rule") name = base._random_name("rule")
if filters is None: if filters is None:
filters = [] filters = []
client = self._get_client(**kwargs) policy = v2_swagger_client.ReplicationPolicy(name=name, description=description,dest_namespace=dest_namespace,
policy = swagger_client.ReplicationPolicy(name=name, description=description,dest_namespace=dest_namespace, dest_registry=dest_registry, src_registry=src_registry,filters=filters,
dest_registry=dest_registry, src_registry=src_registry,filters=filters, trigger=trigger, deletion=deletion, override=override, enabled=enabled)
trigger=trigger, deletion=deletion, override=override, enabled=enabled) _, status_code, header = self._get_client(**kwargs).create_replication_policy_with_http_info(policy)
_, status_code, header = client.replication_policies_post_with_http_info(policy)
base._assert_status_code(expect_status_code, status_code) base._assert_status_code(expect_status_code, status_code)
return base._get_id_from_header(header), name return base._get_id_from_header(header), name
def get_replication_rule(self, param = None, rule_id = None, expect_status_code = 200, **kwargs): 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 rule_id is None:
if param is None: if param is None:
param = dict() 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: 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) base._assert_status_code(expect_status_code, status_code)
return data 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)) # 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): def delete_replication_rule(self, rule_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs) _, status_code, _ = self._get_client(**kwargs).delete_replication_policy_with_http_info(rule_id)
_, status_code, _ = client.replication_policies_id_delete_with_http_info(rule_id) base._assert_status_code(expect_status_code, status_code)
base._assert_status_code(expect_status_code, status_code)

View File

@ -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

View File

@ -9,8 +9,8 @@ from library.replication import Replication
from library.registry import Registry from library.registry import Registry
from library.artifact import Artifact from library.artifact import Artifact
from library.repository import Repository from library.repository import Repository
from library.replication_v2 import ReplicationV2
import swagger_client import swagger_client
import v2_swagger_client
from testutils import DOCKER_USER, DOCKER_PWD from testutils import DOCKER_USER, DOCKER_PWD
class TestProjects(unittest.TestCase): class TestProjects(unittest.TestCase):
@ -19,7 +19,6 @@ class TestProjects(unittest.TestCase):
self.project = Project() self.project = Project()
self.user = User() self.user = User()
self.replication = Replication() self.replication = Replication()
self.replication_v2 = ReplicationV2()
self.registry = Registry() self.registry = Registry()
self.artifact = Artifact() self.artifact = Artifact()
self.repo = Repository() self.repo = Repository()
@ -83,17 +82,17 @@ class TestProjects(unittest.TestCase):
#4. Create a pull-based rule for this registry; #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)), 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, 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) **ADMIN_CLIENT)
#5. Check rule should be exist; #5. Check rule should be exist;
self.replication.check_replication_rule_should_exist(TestProjects.rule_id, rule_name, **ADMIN_CLIENT) self.replication.check_replication_rule_should_exist(TestProjects.rule_id, rule_name, **ADMIN_CLIENT)
#6. Trigger the rule; #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; #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. #8. Check image is replicated into target project successfully.
artifact = self.artifact.get_reference_info(TestProjects.project_name, self.image, self.tag, **ADMIN_CLIENT) artifact = self.artifact.get_reference_info(TestProjects.project_name, self.image, self.tag, **ADMIN_CLIENT)

View File

@ -159,4 +159,4 @@ Test Case - Metrics
Test Case - Project Level Policy Content Trust Test Case - Project Level Policy Content Trust
[Tags] 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