mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 17:47:46 +01:00
Merge pull request #7079 from ywk253100/190304_adapter_api
Implement replication adapter API
This commit is contained in:
commit
2cce835784
@ -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
|
||||
|
@ -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")
|
||||
|
57
src/core/api/replication_adapter.go
Normal file
57
src/core/api/replication_adapter.go
Normal 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)
|
||||
}
|
110
src/core/api/replication_adapter_test.go
Normal file
110
src/core/api/replication_adapter_test.go
Normal 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...)
|
||||
}
|
@ -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{})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user