diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 82174155e..6e5ec14cb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 1e2b8f001..5db28c63f 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -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") diff --git a/src/core/api/replication_execution_test.go b/src/core/api/replication_execution_test.go index 6f7a3313c..ec9bafdbe 100644 --- a/src/core/api/replication_execution_test.go +++ b/src/core/api/replication_execution_test.go @@ -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 } diff --git a/src/core/api/replication_policy_ng.go b/src/core/api/replication_policy_ng.go new file mode 100644 index 000000000..f7410ada8 --- /dev/null +++ b/src/core/api/replication_policy_ng.go @@ -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 + } +} diff --git a/src/core/api/replication_policy_ng_test.go b/src/core/api/replication_policy_ng_test.go new file mode 100644 index 000000000..53a630eff --- /dev/null +++ b/src/core/api/replication_policy_ng_test.go @@ -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...) +} diff --git a/src/core/router.go b/src/core/router.go index d9545f225..f0cbec0b7 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -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{}) diff --git a/src/replication/ng/dao/policy.go b/src/replication/ng/dao/policy.go index ce8ee20bf..24cdb7386 100644 --- a/src/replication/ng/dao/policy.go +++ b/src/replication/ng/dao/policy.go @@ -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 } diff --git a/src/replication/ng/dao/policy_test.go b/src/replication/ng/dao/policy_test.go index 91a8bcac4..8fa9895fc 100644 --- a/src/replication/ng/dao/policy_test.go +++ b/src/replication/ng/dao/policy_test.go @@ -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) }) } } diff --git a/src/replication/ng/dao/registry.go b/src/replication/ng/dao/registry.go index aa0eb8676..75ea690e0 100644 --- a/src/replication/ng/dao/registry.go +++ b/src/replication/ng/dao/registry.go @@ -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 } diff --git a/src/replication/ng/model/policy.go b/src/replication/ng/model/policy.go index 1e2055c81..232441319 100644 --- a/src/replication/ng/model/policy.go +++ b/src/replication/ng/model/policy.go @@ -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 diff --git a/src/replication/ng/model/registry.go b/src/replication/ng/model/registry.go index 5b34efcac..cc4f82f9b 100644 --- a/src/replication/ng/model/registry.go +++ b/src/replication/ng/model/registry.go @@ -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 diff --git a/src/replication/ng/policy/manager.go b/src/replication/ng/policy/manager.go index 14efcb15e..50eb16d01 100644 --- a/src/replication/ng/policy/manager.go +++ b/src/replication/ng/policy/manager.go @@ -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) diff --git a/src/replication/ng/policy/manager_test.go b/src/replication/ng/policy/manager_test.go index 3b5a64579..03b1103f4 100644 --- a/src/replication/ng/policy/manager_test.go +++ b/src/replication/ng/policy/manager_test.go @@ -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) diff --git a/src/replication/ng/registry/manager.go b/src/replication/ng/registry/manager.go index 41b62c65d..07f0bf462 100644 --- a/src/replication/ng/registry/manager.go +++ b/src/replication/ng/registry/manager.go @@ -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{ diff --git a/src/replication/ng/replication.go b/src/replication/ng/replication.go index abce84e54..03c36979e 100644 --- a/src/replication/ng/replication.go +++ b/src/replication/ng/replication.go @@ -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