Implement replication adapter API

This commit implements the replication adapter API

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2019-03-04 18:33:20 +08:00
parent c9498410a9
commit 7f49151115
9 changed files with 363 additions and 35 deletions

View File

@ -2080,6 +2080,53 @@ paths:
$ref: '#/responses/UnsupportedMediaType' $ref: '#/responses/UnsupportedMediaType'
'500': '500':
description: Unexpected internal errors. 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: /registries:
get: get:
summary: List registries. summary: List registries.
@ -4746,3 +4793,23 @@ definitions:
action: action:
type: string type: string
description: The permission action 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

View File

@ -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/", &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/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 // Charts are controlled under projects
chartRepositoryAPIType := &ChartRepositoryAPI{} chartRepositoryAPIType := &ChartRepositoryAPI{}
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus") beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")

View File

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

View File

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

View File

@ -99,6 +99,9 @@ func initRouters() {
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post") beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
beego.Router("/api/logs", &api.LogAPI{}) 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/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put") beego.Router("/api/configurations", &api.ConfigAPI{}, "get:Get;put:Put")
beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/statistics", &api.StatisticAPI{})

View File

@ -21,23 +21,28 @@ import (
"github.com/goharbor/harbor/src/replication/ng/model" "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 item struct {
type Factory func(*model.Registry) (Adapter, error) info *Info
factory Factory
}
// Info provides base info and capability declarations of the adapter // Info provides base info and capability declarations of the adapter
type Info struct { type Info struct {
Name model.RegistryType `json:"name"` Type model.RegistryType `json:"type"`
Description string `json:"description"` Description string `json:"description"`
SupportedResourceTypes []model.ResourceType `json:"supported_resource_types"` SupportedResourceTypes []model.ResourceType `json:"supported_resource_types"`
SupportedResourceFilters []model.FilterType `json:"supported_resource_filters"` 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 // Adapter interface defines the capabilities of registry
type Adapter interface { type Adapter interface {
// Info return the information of this adapter
Info() *Info
// Lists the available namespaces under the specified registry with the // Lists the available namespaces under the specified registry with the
// provided credential/token // provided credential/token
ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error)
@ -55,26 +60,53 @@ type Adapter interface {
} }
// RegisterFactory registers one adapter factory to the registry // RegisterFactory registers one adapter factory to the registry
func RegisterFactory(name model.RegistryType, factory Factory) error { func RegisterFactory(info *Info, factory Factory) error {
if !name.Valid() { if len(info.Type) == 0 {
return errors.New("invalid adapter factory name") return errors.New("invalid registry type")
}
if len(info.SupportedResourceTypes) == 0 {
return errors.New("must support at least one resource type")
} }
if factory == nil { if factory == nil {
return errors.New("empty adapter factory") return errors.New("empty adapter factory")
} }
if _, exist := registry[name]; exist { for _, item := range registry {
return fmt.Errorf("adapter factory for %s already exists", name) 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 return nil
} }
// GetFactory gets the adapter factory by the specified name // GetFactory gets the adapter factory by the specified name
func GetFactory(name model.RegistryType) (Factory, error) { func GetFactory(t model.RegistryType) (Factory, error) {
factory, exist := registry[name] for _, item := range registry {
if !exist { if item.info.Type == t {
return nil, fmt.Errorf("adapter factory for %s not found", name) return item.factory, nil
}
} }
return nil, fmt.Errorf("adapter factory for %s not found", t)
return factory, nil }
// 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
} }

View File

@ -27,23 +27,80 @@ func fakedFactory(*model.Registry) (Adapter, error) {
} }
func TestRegisterFactory(t *testing.T) { func TestRegisterFactory(t *testing.T) {
// empty name // empty type
assert.NotNil(t, RegisterFactory("", nil)) assert.NotNil(t, RegisterFactory(&Info{}, nil))
// empty supportted resource type
assert.NotNil(t, RegisterFactory(
&Info{
Type: "harbor",
}, nil))
// empty factory // empty factory
assert.NotNil(t, RegisterFactory("factory", nil)) assert.NotNil(t, RegisterFactory(
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
}, nil))
// pass // pass
assert.Nil(t, RegisterFactory("factory", fakedFactory)) assert.Nil(t, RegisterFactory(
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
}, fakedFactory))
// already exists // 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) { func TestGetFactory(t *testing.T) {
registry = map[model.RegistryType]Factory{} registry = []*item{}
require.Nil(t, RegisterFactory("factory", fakedFactory)) require.Nil(t, RegisterFactory(
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
}, fakedFactory))
// doesn't exist // doesn't exist
_, err := GetFactory("another_factory") _, err := GetFactory("gcr")
assert.NotNil(t, err) assert.NotNil(t, err)
// pass // pass
_, err = GetFactory("factory") _, err = GetFactory("harbor")
assert.Nil(t, err) 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))
}

View File

@ -185,7 +185,11 @@ func (f *fakedAdapter) FetchResources(namespace []string, filters []*model.Filte
} }
func TestStartReplication(t *testing.T) { 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) require.Nil(t, err)
controller, _ := NewController( controller, _ := NewController(

View File

@ -20,18 +20,13 @@ import (
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
) )
// RegistryType indicates the type of registry
type RegistryType string
const ( const (
// RegistryTypeHarbor indicates registry type harbor // RegistryTypeHarbor indicates registry type harbor
RegistryTypeHarbor = "harbor" RegistryTypeHarbor = "harbor"
) )
// Valid indicates whether the RegistryType is a valid value // RegistryType indicates the type of registry
func (r RegistryType) Valid() bool { type RegistryType string
return len(r) > 0
}
// CredentialType represents the supported credential types // CredentialType represents the supported credential types
// e.g: u/p, OAuth token // e.g: u/p, OAuth token