From 7f49151115503a482096e41cc0865149cd3638f0 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Mon, 4 Mar 2019 18:33:20 +0800 Subject: [PATCH] Implement replication adapter API This commit implements the replication adapter API Signed-off-by: Wenkai Yin --- docs/swagger.yaml | 67 +++++++++++++ src/core/api/harborapi_test.go | 3 + src/core/api/replication_adapter.go | 57 +++++++++++ src/core/api/replication_adapter_test.go | 110 +++++++++++++++++++++ src/core/router.go | 3 + src/replication/ng/adapter/adapter.go | 68 +++++++++---- src/replication/ng/adapter/adapter_test.go | 75 ++++++++++++-- src/replication/ng/flow/controller_test.go | 6 +- src/replication/ng/model/registry.go | 9 +- 9 files changed, 363 insertions(+), 35 deletions(-) create mode 100644 src/core/api/replication_adapter.go create mode 100644 src/core/api/replication_adapter_test.go diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f57bab2ad..6d5afb2fc 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2080,6 +2080,53 @@ paths: $ref: '#/responses/UnsupportedMediaType' '500': description: Unexpected internal errors. + /replication/adapters: + get: + summary: List supported adapters. + description: | + This endpoint let user list supported adapters. + tags: + - Products + responses: + '200': + description: Success. + schema: + type: array + items: + $ref: '#/definitions/ReplicationAdapter' + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Unexpected internal errors. + /replication/adapters/{type}: + get: + summary: Get the specified adapter. + description: | + This endpoint let user get the specified adapter. + parameters: + - name: type + in: path + type: string + required: true + description: The adapter type. + tags: + - Products + responses: + '200': + description: Success. + schema: + type: object + $ref: '#/definitions/ReplicationAdapter' + '401': + description: Unauthorized. + '403': + description: Forbidden. + '404': + description: Not found. + '500': + description: Unexpected internal errors. /registries: get: summary: List registries. @@ -4746,3 +4793,23 @@ definitions: action: type: string description: The permission action + ReplicationAdapter: + type: object + description: The replication adapter + properties: + type: + type: string + description: The adapter type + description: + type: string + description: The adapter description + supported_resource_types: + type: array + description: The resource types that the adapter supports + items: + type: string + supported_resource_filters: + type: array + description: The filters that the adapter supports + items: + type: string diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 3201b1e8a..5492aceeb 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -151,6 +151,9 @@ func init() { beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List") beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete") + beego.Router("/api/replication/adapters", &ReplicationAdapterAPI{}, "get:List") + beego.Router("/api/replication/adapters/:type", &ReplicationAdapterAPI{}, "get:Get") + // Charts are controlled under projects chartRepositoryAPIType := &ChartRepositoryAPI{} beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus") diff --git a/src/core/api/replication_adapter.go b/src/core/api/replication_adapter.go new file mode 100644 index 000000000..1882d5df5 --- /dev/null +++ b/src/core/api/replication_adapter.go @@ -0,0 +1,57 @@ +// 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" + + "github.com/goharbor/harbor/src/replication/ng/adapter" + "github.com/goharbor/harbor/src/replication/ng/model" +) + +// ReplicationAdapterAPI handles the replication adapter requests +type ReplicationAdapterAPI struct { + BaseController +} + +// Prepare ... +func (r *ReplicationAdapterAPI) Prepare() { + r.BaseController.Prepare() + if !r.SecurityCtx.IsSysAdmin() { + if !r.SecurityCtx.IsAuthenticated() { + r.HandleUnauthorized() + return + } + r.HandleForbidden(r.SecurityCtx.GetUsername()) + return + } +} + +// List the replication adapters +func (r *ReplicationAdapterAPI) List() { + infos := adapter.ListAdapterInfos() + r.WriteJSONData(infos) +} + +// Get one specified replication adapter +func (r *ReplicationAdapterAPI) Get() { + t := r.GetStringFromPath(":type") + info := adapter.GetAdapterInfo(model.RegistryType(t)) + if info == nil { + r.HandleNotFound(fmt.Sprintf("adapter for %s not found", t)) + return + } + r.WriteJSONData(info) +} diff --git a/src/core/api/replication_adapter_test.go b/src/core/api/replication_adapter_test.go new file mode 100644 index 000000000..3a9217479 --- /dev/null +++ b/src/core/api/replication_adapter_test.go @@ -0,0 +1,110 @@ +// 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/stretchr/testify/require" + + "github.com/goharbor/harbor/src/replication/ng/adapter" + "github.com/goharbor/harbor/src/replication/ng/model" +) + +func TestReplicationAdapterAPIList(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/replication/adapters", + }, + code: http.StatusUnauthorized, + }, + // 403 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/replication/adapters", + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/replication/adapters", + credential: sysAdmin, + }, + code: http.StatusOK, + }, + } + + runCodeCheckingCases(t, cases...) +} + +func fakedFactory(*model.Registry) (adapter.Adapter, error) { + return nil, nil +} +func TestReplicationAdapterAPIGet(t *testing.T) { + err := adapter.RegisterFactory( + &adapter.Info{ + Type: "harbor", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, fakedFactory) + require.Nil(t, err) + + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/replication/adapters/harbor", + }, + code: http.StatusUnauthorized, + }, + // 403 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/replication/adapters/harbor", + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + // 404 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/replication/adapters/gcs", + credential: sysAdmin, + }, + code: http.StatusNotFound, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/replication/adapters/harbor", + credential: sysAdmin, + }, + code: http.StatusOK, + }, + } + + runCodeCheckingCases(t, cases...) +} diff --git a/src/core/router.go b/src/core/router.go index 5d76961bc..f919de4b7 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -99,6 +99,9 @@ func initRouters() { beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post") beego.Router("/api/logs", &api.LogAPI{}) + beego.Router("/api/replication/adapters", &api.ReplicationAdapterAPI{}, "get:List") + beego.Router("/api/replication/adapters/:type", &api.ReplicationAdapterAPI{}, "get:Get") + 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/adapter/adapter.go b/src/replication/ng/adapter/adapter.go index c60ebf2a2..7408249f1 100644 --- a/src/replication/ng/adapter/adapter.go +++ b/src/replication/ng/adapter/adapter.go @@ -21,23 +21,28 @@ import ( "github.com/goharbor/harbor/src/replication/ng/model" ) -var registry = map[model.RegistryType]Factory{} +// As the Info isn't a valid map key, so we use the slice +// as the adapter registry +var registry = []*item{} -// Factory creates a specific Adapter according to the params -type Factory func(*model.Registry) (Adapter, error) +type item struct { + info *Info + factory Factory +} // Info provides base info and capability declarations of the adapter type Info struct { - Name model.RegistryType `json:"name"` + Type model.RegistryType `json:"type"` Description string `json:"description"` SupportedResourceTypes []model.ResourceType `json:"supported_resource_types"` SupportedResourceFilters []model.FilterType `json:"supported_resource_filters"` } +// Factory creates a specific Adapter according to the params +type Factory func(*model.Registry) (Adapter, error) + // Adapter interface defines the capabilities of registry type Adapter interface { - // Info return the information of this adapter - Info() *Info // Lists the available namespaces under the specified registry with the // provided credential/token ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) @@ -55,26 +60,53 @@ type Adapter interface { } // RegisterFactory registers one adapter factory to the registry -func RegisterFactory(name model.RegistryType, factory Factory) error { - if !name.Valid() { - return errors.New("invalid adapter factory name") +func RegisterFactory(info *Info, factory Factory) error { + if len(info.Type) == 0 { + return errors.New("invalid registry type") + } + if len(info.SupportedResourceTypes) == 0 { + return errors.New("must support at least one resource type") } if factory == nil { return errors.New("empty adapter factory") } - if _, exist := registry[name]; exist { - return fmt.Errorf("adapter factory for %s already exists", name) + for _, item := range registry { + if item.info.Type == info.Type { + return fmt.Errorf("adapter factory for %s already exists", info.Type) + } } - registry[name] = factory + registry = append(registry, &item{ + info: info, + factory: factory, + }) return nil } // GetFactory gets the adapter factory by the specified name -func GetFactory(name model.RegistryType) (Factory, error) { - factory, exist := registry[name] - if !exist { - return nil, fmt.Errorf("adapter factory for %s not found", name) +func GetFactory(t model.RegistryType) (Factory, error) { + for _, item := range registry { + if item.info.Type == t { + return item.factory, nil + } } - - return factory, nil + return nil, fmt.Errorf("adapter factory for %s not found", t) +} + +// ListAdapterInfos lists the info of registered Adapters +func ListAdapterInfos() []*Info { + infos := []*Info{} + for _, item := range registry { + infos = append(infos, item.info) + } + return infos +} + +// GetAdapterInfo returns the info of a specified registry type +func GetAdapterInfo(t model.RegistryType) *Info { + for _, item := range registry { + if item.info.Type == t { + return item.info + } + } + return nil } diff --git a/src/replication/ng/adapter/adapter_test.go b/src/replication/ng/adapter/adapter_test.go index 73a20014b..3896963d1 100644 --- a/src/replication/ng/adapter/adapter_test.go +++ b/src/replication/ng/adapter/adapter_test.go @@ -27,23 +27,80 @@ func fakedFactory(*model.Registry) (Adapter, error) { } func TestRegisterFactory(t *testing.T) { - // empty name - assert.NotNil(t, RegisterFactory("", nil)) + // empty type + assert.NotNil(t, RegisterFactory(&Info{}, nil)) + // empty supportted resource type + assert.NotNil(t, RegisterFactory( + &Info{ + Type: "harbor", + }, nil)) // empty factory - assert.NotNil(t, RegisterFactory("factory", nil)) + assert.NotNil(t, RegisterFactory( + &Info{ + Type: "harbor", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, nil)) // pass - assert.Nil(t, RegisterFactory("factory", fakedFactory)) + assert.Nil(t, RegisterFactory( + &Info{ + Type: "harbor", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, fakedFactory)) // already exists - assert.NotNil(t, RegisterFactory("factory", fakedFactory)) + assert.NotNil(t, RegisterFactory( + &Info{ + Type: "harbor", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, fakedFactory)) } func TestGetFactory(t *testing.T) { - registry = map[model.RegistryType]Factory{} - require.Nil(t, RegisterFactory("factory", fakedFactory)) + registry = []*item{} + require.Nil(t, RegisterFactory( + &Info{ + Type: "harbor", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, fakedFactory)) // doesn't exist - _, err := GetFactory("another_factory") + _, err := GetFactory("gcr") assert.NotNil(t, err) // pass - _, err = GetFactory("factory") + _, err = GetFactory("harbor") assert.Nil(t, err) } + +func TestListAdapterInfos(t *testing.T) { + registry = []*item{} + // not register, got nothing + infos := ListAdapterInfos() + assert.Equal(t, 0, len(infos)) + + // register one factory + require.Nil(t, RegisterFactory( + &Info{ + Type: "harbor", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, fakedFactory)) + + infos = ListAdapterInfos() + require.Equal(t, 1, len(infos)) + assert.Equal(t, "harbor", string(infos[0].Type)) +} + +func TestGetAdapterInfo(t *testing.T) { + registry = []*item{} + require.Nil(t, RegisterFactory( + &Info{ + Type: "harbor", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, fakedFactory)) + + // doesn't exist + info := GetAdapterInfo("gcr") + assert.Nil(t, info) + + // exist + info = GetAdapterInfo("harbor") + require.NotNil(t, info) + assert.Equal(t, "harbor", string(info.Type)) +} diff --git a/src/replication/ng/flow/controller_test.go b/src/replication/ng/flow/controller_test.go index f3d0f6c64..1d758955d 100644 --- a/src/replication/ng/flow/controller_test.go +++ b/src/replication/ng/flow/controller_test.go @@ -185,7 +185,11 @@ func (f *fakedAdapter) FetchResources(namespace []string, filters []*model.Filte } func TestStartReplication(t *testing.T) { - err := adapter.RegisterFactory("faked_registry", fakedAdapterFactory) + err := adapter.RegisterFactory( + &adapter.Info{ + Type: "faked_registry", + SupportedResourceTypes: []model.ResourceType{"image"}, + }, fakedAdapterFactory) require.Nil(t, err) controller, _ := NewController( diff --git a/src/replication/ng/model/registry.go b/src/replication/ng/model/registry.go index a6cddc085..5b34efcac 100644 --- a/src/replication/ng/model/registry.go +++ b/src/replication/ng/model/registry.go @@ -20,18 +20,13 @@ import ( "github.com/goharbor/harbor/src/common/models" ) -// RegistryType indicates the type of registry -type RegistryType string - const ( // RegistryTypeHarbor indicates registry type harbor RegistryTypeHarbor = "harbor" ) -// Valid indicates whether the RegistryType is a valid value -func (r RegistryType) Valid() bool { - return len(r) > 0 -} +// RegistryType indicates the type of registry +type RegistryType string // CredentialType represents the supported credential types // e.g: u/p, OAuth token