mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 18:25:56 +01:00
Implement replication policy management API
This commit implements the replication policy management API Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
parent
52eb89c6f2
commit
d1f4c20e64
@ -2035,6 +2035,167 @@ paths:
|
||||
description: The resource does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/replication/policies:
|
||||
get:
|
||||
summary: List replication policies
|
||||
description: |
|
||||
This endpoint let user list replication policies
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: The replication policy name.
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: The page nubmer.
|
||||
- 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'
|
||||
'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'
|
||||
'404':
|
||||
$ref: '#/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/responses/Conflict'
|
||||
'415':
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
'/replication/policies/{id}':
|
||||
get:
|
||||
summary: Get replication policy.
|
||||
description: |
|
||||
This endpoint let user get replication policy by specific ID.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: policy ID
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get the replication policy successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
put:
|
||||
summary: Update the replication policy
|
||||
description: |
|
||||
This endpoint let user update policy.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: policy ID
|
||||
- name: policy
|
||||
in: body
|
||||
description: The replication policy model.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ReplicationPolicy'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/OK'
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/responses/Conflict'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
delete:
|
||||
summary: Delete the replication policy specified by ID.
|
||||
description: |
|
||||
Delete the replication policy specified by ID.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Replication policy ID
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/responses/OK'
|
||||
'400':
|
||||
$ref: '#/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/responses/NotFound'
|
||||
'412':
|
||||
$ref: '#/responses/PreconditionFailed'
|
||||
'500':
|
||||
$ref: '#/responses/InternalServerError'
|
||||
/labels:
|
||||
get:
|
||||
summary: List labels according to the query strings.
|
||||
@ -3505,8 +3666,26 @@ paths:
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
responses:
|
||||
OK:
|
||||
description: 'Success'
|
||||
Created:
|
||||
description: 'Created'
|
||||
BadRequest:
|
||||
description: 'Bad Request'
|
||||
Unauthorized:
|
||||
description: 'Unauthorized'
|
||||
Forbidden:
|
||||
description: 'Forbidden'
|
||||
NotFound:
|
||||
description: 'Not Found'
|
||||
Conflict:
|
||||
description: 'Conflict'
|
||||
PreconditionFailed:
|
||||
description: 'Precondition Failed'
|
||||
UnsupportedMediaType:
|
||||
description: 'The Media Type of the request is not supported, it has to be "application/json"'
|
||||
InternalServerError:
|
||||
description: 'Internal Server Error'
|
||||
definitions:
|
||||
Search:
|
||||
type: object
|
||||
@ -3836,6 +4015,57 @@ definitions:
|
||||
error_job_count:
|
||||
type: integer
|
||||
description: The error job count number for the policy.
|
||||
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_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The source registry ID.
|
||||
src_namespaces:
|
||||
type: array
|
||||
description: The source namespaces
|
||||
items:
|
||||
type: string
|
||||
dest_registry_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The destination registry ID.
|
||||
dest_namespace:
|
||||
type: string
|
||||
description: The destination namespace.
|
||||
trigger:
|
||||
$ref: '#/definitions/RepTrigger'
|
||||
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.
|
||||
RepTrigger:
|
||||
type: object
|
||||
properties:
|
||||
@ -3873,6 +4103,15 @@ definitions:
|
||||
metadata:
|
||||
type: object
|
||||
description: This map object is the replication policy filter metadata.
|
||||
ReplicationFilter:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 'The replication policy filter type.'
|
||||
value:
|
||||
type: string
|
||||
description: 'The value of replication policy filter.'
|
||||
RegistryCredential:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -158,6 +158,9 @@ func init() {
|
||||
beego.Router("/api/replication/executions/:id([0-9]+)/tasks", &ReplicationOperationAPI{}, "get:ListTasks")
|
||||
beego.Router("/api/replication/executions/:id([0-9]+)/tasks/:tid([0-9]+)/log", &ReplicationOperationAPI{}, "get:GetTaskLog")
|
||||
|
||||
beego.Router("/api/replication/policies", &ReplicationPolicyAPI{}, "get:List;post:Create")
|
||||
beego.Router("/api/replication/policies/:id([0-9]+)", &ReplicationPolicyAPI{}, "get:Get;put:Update;delete:Delete")
|
||||
|
||||
// Charts are controlled under projects
|
||||
chartRepositoryAPIType := &ChartRepositoryAPI{}
|
||||
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
||||
|
@ -87,6 +87,14 @@ func (f *fakedPolicyManager) Get(id int64) (*model.Policy, error) {
|
||||
}
|
||||
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, ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
214
src/core/api/replication_policy_ng.go
Normal file
214
src/core/api/replication_policy_ng.go
Normal file
@ -0,0 +1,214 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/ng"
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
)
|
||||
|
||||
// TODO rename the file to "replication.go"
|
||||
|
||||
// ReplicationPolicyAPI handles the replication policy requests
|
||||
type ReplicationPolicyAPI struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
// Prepare ...
|
||||
func (r *ReplicationPolicyAPI) Prepare() {
|
||||
r.BaseController.Prepare()
|
||||
if !r.SecurityCtx.IsSysAdmin() {
|
||||
if !r.SecurityCtx.IsAuthenticated() {
|
||||
r.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
r.HandleForbidden(r.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// List the replication policies
|
||||
func (r *ReplicationPolicyAPI) List() {
|
||||
// TODO: support more query
|
||||
query := &model.PolicyQuery{
|
||||
Name: r.GetString("name"),
|
||||
}
|
||||
query.Page, query.Size = r.GetPaginationParams()
|
||||
|
||||
total, policies, err := ng.PolicyMgr.List(query)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to list policies: %v", err))
|
||||
return
|
||||
}
|
||||
r.SetPaginationHeader(total, query.Page, query.Size)
|
||||
r.WriteJSONData(policies)
|
||||
}
|
||||
|
||||
// Create the replication policy
|
||||
func (r *ReplicationPolicyAPI) Create() {
|
||||
policy := &model.Policy{}
|
||||
r.DecodeJSONReqAndValidate(policy)
|
||||
|
||||
if !r.validateName(policy) {
|
||||
return
|
||||
}
|
||||
if !r.validateRegistry(policy) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := ng.PolicyMgr.Create(policy)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to create the policy: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO handle replication_now?
|
||||
|
||||
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 := ng.PolicyMgr.GetByName(policy.Name)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get policy %s: %v", policy.Name, err))
|
||||
return false
|
||||
}
|
||||
if p != nil {
|
||||
r.HandleConflict(fmt.Sprintf("policy %s already exists", policy.Name))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// make the registry referenced exists
|
||||
func (r *ReplicationPolicyAPI) validateRegistry(policy *model.Policy) bool {
|
||||
registryID := policy.SrcRegistryID
|
||||
if registryID == 0 {
|
||||
registryID = policy.DestRegistryID
|
||||
}
|
||||
registry, err := ng.RegistryMgr.Get(registryID)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get registry %d: %v", registryID, err))
|
||||
return false
|
||||
}
|
||||
if registry == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("registry %d not found", registryID))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO validate trigger in create and update
|
||||
|
||||
// Get the specified replication policy
|
||||
func (r *ReplicationPolicyAPI) Get() {
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if id <= 0 || err != nil {
|
||||
r.HandleBadRequest("invalid policy ID")
|
||||
return
|
||||
}
|
||||
|
||||
policy, err := ng.PolicyMgr.Get(id)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
if policy == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("policy %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
r.WriteJSONData(policy)
|
||||
}
|
||||
|
||||
// Update the replication policy
|
||||
func (r *ReplicationPolicyAPI) Update() {
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if id <= 0 || err != nil {
|
||||
r.HandleBadRequest("invalid policy ID")
|
||||
return
|
||||
}
|
||||
|
||||
originalPolicy, err := ng.PolicyMgr.Get(id)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
if originalPolicy == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("policy %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
policy := &model.Policy{}
|
||||
r.DecodeJSONReqAndValidate(policy)
|
||||
if policy.Name != originalPolicy.Name &&
|
||||
!r.validateName(policy) {
|
||||
return
|
||||
}
|
||||
|
||||
if !r.validateRegistry(policy) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO passing the properties need to be updated?
|
||||
if err := ng.PolicyMgr.Update(policy); err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to update the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the replication policy
|
||||
func (r *ReplicationPolicyAPI) Delete() {
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if id <= 0 || err != nil {
|
||||
r.HandleBadRequest("invalid policy ID")
|
||||
return
|
||||
}
|
||||
|
||||
policy, err := ng.PolicyMgr.Get(id)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
if policy == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("policy %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
_, executions, err := ng.OperationCtl.ListExecutions(&model.ExecutionQuery{
|
||||
PolicyID: id,
|
||||
})
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get the executions of policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, execution := range executions {
|
||||
if execution.Status == model.ExecutionStatusInProgress {
|
||||
r.HandleStatusPreconditionFailed(fmt.Sprintf("the policy %d has running executions, can not be deleted", id))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := ng.PolicyMgr.Remove(id); err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to delete the policy %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
}
|
397
src/core/api/replication_policy_ng_test.go
Normal file
397
src/core/api/replication_policy_ng_test.go
Normal file
@ -0,0 +1,397 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/ng"
|
||||
)
|
||||
|
||||
// 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(...*model.RegistryQuery) (int64, []*model.Registry, error) {
|
||||
return 0, nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Get(id int64) (*model.Registry, error) {
|
||||
if id == 1 {
|
||||
return &model.Registry{
|
||||
Type: "faked_registry",
|
||||
}, nil
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPIList(t *testing.T) {
|
||||
policyMgr := ng.PolicyMgr
|
||||
defer func() {
|
||||
ng.PolicyMgr = policyMgr
|
||||
}()
|
||||
ng.PolicyMgr = &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 := ng.PolicyMgr
|
||||
registryMgr := ng.RegistryMgr
|
||||
defer func() {
|
||||
ng.PolicyMgr = policyMgr
|
||||
ng.RegistryMgr = registryMgr
|
||||
}()
|
||||
ng.PolicyMgr = &fakedPolicyManager{}
|
||||
ng.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{
|
||||
SrcRegistryID: 1,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400 empty registry
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400 empty source namespaces
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistryID: 1,
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 409, duplicate policy name
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "duplicate_name",
|
||||
SrcRegistryID: 1,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusConflict,
|
||||
},
|
||||
// 404, registry not found
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistryID: 2,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 201
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: "/api/replication/policies",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistryID: 1,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusCreated,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPIGet(t *testing.T) {
|
||||
policyMgr := ng.PolicyMgr
|
||||
defer func() {
|
||||
ng.PolicyMgr = policyMgr
|
||||
}()
|
||||
ng.PolicyMgr = &fakedPolicyManager{}
|
||||
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/2",
|
||||
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 := ng.PolicyMgr
|
||||
registryMgr := ng.RegistryMgr
|
||||
defer func() {
|
||||
ng.PolicyMgr = policyMgr
|
||||
ng.RegistryMgr = registryMgr
|
||||
}()
|
||||
ng.PolicyMgr = &fakedPolicyManager{}
|
||||
ng.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/2",
|
||||
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{
|
||||
SrcRegistryID: 1,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
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",
|
||||
SrcRegistryID: 1,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusConflict,
|
||||
},
|
||||
// 404, registry not found
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistryID: 2,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
bodyJSON: &model.Policy{
|
||||
Name: "policy01",
|
||||
SrcRegistryID: 1,
|
||||
SrcNamespaces: []string{"library"},
|
||||
},
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestReplicationPolicyAPIDelete(t *testing.T) {
|
||||
policyMgr := ng.PolicyMgr
|
||||
defer func() {
|
||||
ng.PolicyMgr = policyMgr
|
||||
}()
|
||||
ng.PolicyMgr = &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/2",
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: "/api/replication/policies/1",
|
||||
credential: sysAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
@ -106,6 +106,9 @@ func initRouters() {
|
||||
beego.Router("/api/replication/executions/:id([0-9]+)/tasks", &api.ReplicationOperationAPI{}, "get:ListTasks")
|
||||
beego.Router("/api/replication/executions/:id([0-9]+)/tasks/:tid([0-9]+)/log", &api.ReplicationOperationAPI{}, "get:GetTaskLog")
|
||||
|
||||
beego.Router("/api/replication/policies", &api.ReplicationPolicyAPI{}, "get:List;post:Create")
|
||||
beego.Router("/api/replication/policies/:id([0-9]+)", &api.ReplicationPolicyAPI{}, "get:Get;put:Update;delete:Delete")
|
||||
|
||||
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
|
||||
beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
|
||||
beego.Router("/api/statistics", &api.StatisticAPI{})
|
||||
|
@ -53,6 +53,9 @@ 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
|
||||
}
|
||||
@ -62,6 +65,9 @@ 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
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package dao
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -303,9 +302,9 @@ func TestDeleteRepPolicy(t *testing.T) {
|
||||
}
|
||||
|
||||
require.Nil(t, err)
|
||||
_, err = GetRepPolicy(tt.id)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, err, orm.ErrNoRows)
|
||||
policy, err := GetRepPolicy(tt.id)
|
||||
require.Nil(t, err)
|
||||
assert.Nil(t, policy)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,9 @@ func ListRegistries(query ...*ListRegistryQuery) (int64, []*models.Registry, err
|
||||
if err != nil {
|
||||
return total, nil, err
|
||||
}
|
||||
|
||||
if registries == nil {
|
||||
registries = []*models.Registry{}
|
||||
}
|
||||
return total, registries, nil
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
)
|
||||
|
||||
@ -33,7 +34,8 @@ type Policy struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Creator string `json:"creator"`
|
||||
// TODO consider to remove this property?
|
||||
Creator string `json:"creator"`
|
||||
// source
|
||||
SrcRegistryID int64 `json:"src_registry_id"`
|
||||
SrcNamespaces []string `json:"src_namespaces"`
|
||||
@ -58,6 +60,33 @@ type Policy struct {
|
||||
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")
|
||||
}
|
||||
|
||||
// one of the source registry and destination registry must be Harbor itself
|
||||
if p.SrcRegistryID != 0 && p.DestRegistryID != 0 ||
|
||||
p.SrcRegistryID == 0 && p.DestRegistryID == 0 {
|
||||
v.SetError("src_registry_id, dest_registry_id", "one of them should be empty and the other one shouldn't be empty")
|
||||
}
|
||||
|
||||
// source namespaces cannot be empty
|
||||
if len(p.SrcNamespaces) == 0 {
|
||||
v.SetError("src_namespaces", "cannot be empty")
|
||||
} else {
|
||||
for _, namespace := range p.SrcNamespaces {
|
||||
if len(namespace) == 0 {
|
||||
v.SetError("src_namespaces", "cannot contain empty namespace")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO valid trigger and filters
|
||||
}
|
||||
|
||||
// FilterType represents the type info of the filter.
|
||||
type FilterType string
|
||||
|
||||
|
@ -49,6 +49,8 @@ type Credential struct {
|
||||
AccessSecret string `json:"access_secret"`
|
||||
}
|
||||
|
||||
// TODO add validation for Registry
|
||||
|
||||
// Registry keeps the related info of registry
|
||||
// Data required for the secure access way is not contained here.
|
||||
// DAO layer is not considered here
|
||||
|
@ -29,7 +29,7 @@ var errNilPolicyModel = errors.New("nil policy model")
|
||||
|
||||
func convertFromPersistModel(policy *persist_models.RepPolicy) (*model.Policy, error) {
|
||||
if policy == nil {
|
||||
return &model.Policy{}, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ply := model.Policy{
|
||||
@ -56,7 +56,7 @@ func convertFromPersistModel(policy *persist_models.RepPolicy) (*model.Policy, e
|
||||
if len(policy.Filters) > 0 {
|
||||
filters := []*model.Filter{}
|
||||
if err := json.Unmarshal([]byte(policy.Filters), &filters); err != nil {
|
||||
return &model.Policy{}, err
|
||||
return nil, err
|
||||
}
|
||||
ply.Filters = filters
|
||||
}
|
||||
@ -65,7 +65,7 @@ func convertFromPersistModel(policy *persist_models.RepPolicy) (*model.Policy, e
|
||||
if len(policy.Trigger) > 0 {
|
||||
trigger := &model.Trigger{}
|
||||
if err := json.Unmarshal([]byte(policy.Trigger), trigger); err != nil {
|
||||
return &model.Policy{}, err
|
||||
return nil, err
|
||||
}
|
||||
ply.Trigger = trigger
|
||||
}
|
||||
@ -121,6 +121,8 @@ type Manager interface {
|
||||
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, the "props" are the properties of policy
|
||||
// that need to be updated
|
||||
Update(policy *model.Policy, props ...string) error
|
||||
@ -150,7 +152,7 @@ func (m *DefaultManager) Create(policy *model.Policy) (int64, error) {
|
||||
}
|
||||
|
||||
// List returns all the policies
|
||||
func (m *DefaultManager) List(queries ...*model.PolicyQuery) (total int64, polices []*model.Policy, err error) {
|
||||
func (m *DefaultManager) List(queries ...*model.PolicyQuery) (total int64, policies []*model.Policy, err error) {
|
||||
// default query parameters
|
||||
var name = ""
|
||||
var namespace = ""
|
||||
@ -180,7 +182,11 @@ func (m *DefaultManager) List(queries ...*model.PolicyQuery) (total int64, polic
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
polices = append(polices, ply)
|
||||
policies = append(policies, ply)
|
||||
}
|
||||
|
||||
if policies == nil {
|
||||
policies = []*model.Policy{}
|
||||
}
|
||||
|
||||
return
|
||||
@ -190,7 +196,17 @@ func (m *DefaultManager) List(queries ...*model.PolicyQuery) (total int64, polic
|
||||
func (m *DefaultManager) Get(policyID int64) (*model.Policy, error) {
|
||||
policy, err := dao.GetRepPolicy(policyID)
|
||||
if err != nil {
|
||||
return &model.Policy{}, err
|
||||
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)
|
||||
|
@ -31,16 +31,21 @@ func Test_convertFromPersistModel(t *testing.T) {
|
||||
want *model.Policy
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Nil Persist Model", from: nil, want: &model.Policy{}},
|
||||
{
|
||||
name: "Nil Persist Model",
|
||||
from: nil,
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "parse Filters Error",
|
||||
from: &persist_models.RepPolicy{Filters: "abc"},
|
||||
want: &model.Policy{}, wantErr: true,
|
||||
want: nil, wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "parse Trigger Error",
|
||||
from: &persist_models.RepPolicy{Trigger: "abc"},
|
||||
want: &model.Policy{}, wantErr: true,
|
||||
want: nil, wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Persist Model", from: &persist_models.RepPolicy{
|
||||
@ -83,6 +88,11 @@ func Test_convertFromPersistModel(t *testing.T) {
|
||||
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)
|
||||
|
@ -298,9 +298,13 @@ func fromDaoModel(registry *models.Registry) (*model.Registry, error) {
|
||||
// toDaoModel converts registry model from replication to DAO layer model.
|
||||
// Also, if access secret is provided, encrypt it.
|
||||
func toDaoModel(registry *model.Registry) (*models.Registry, error) {
|
||||
encrypted, err := encrypt(registry.Credential.AccessSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var encrypted string
|
||||
var err error
|
||||
if registry.Credential != nil {
|
||||
encrypted, err = encrypt(registry.Credential.AccessSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &models.Registry{
|
||||
|
@ -45,7 +45,8 @@ var (
|
||||
func Init() error {
|
||||
// Init registry manager
|
||||
RegistryMgr = registry.NewDefaultManager()
|
||||
// TODO init PolicyMgr
|
||||
// init policy manager
|
||||
PolicyMgr = policy.NewDefaultManager()
|
||||
|
||||
// TODO init ExecutionMgr
|
||||
var executionMgr execution.Manager
|
||||
|
Loading…
Reference in New Issue
Block a user