Update listing/getting replication adapter API

This commit updates the listing/getting replication adapter API

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2019-03-20 02:22:11 +08:00
parent bb76a4d97d
commit c65d5e6669
9 changed files with 235 additions and 5 deletions

View File

@ -5261,6 +5261,26 @@ definitions:
supported_resource_filters:
type: array
description: The filters that the adapter supports
items:
$ref: '#/definitions/ReplicationAdapterFilter'
supported_triggers:
type: array
description: The triggers that the adapter supports
items:
type: string
ReplicationAdapterFilter:
type: object
description: The replication adapter filter
properties:
type:
type: string
description: The filter type
style:
type: string
description: The filter style
values:
type: array
description: The filter values
items:
type: string
ReplicationExecution:

View File

@ -41,7 +41,10 @@ func (r *ReplicationAdapterAPI) Prepare() {
// List the replication adapters
func (r *ReplicationAdapterAPI) List() {
infos := adapter.ListAdapterInfos()
infos := []*adapter.Info{}
for _, info := range adapter.ListAdapterInfos() {
infos = append(infos, process(info))
}
r.WriteJSONData(infos)
}
@ -53,5 +56,38 @@ func (r *ReplicationAdapterAPI) Get() {
r.HandleNotFound(fmt.Sprintf("adapter for %s not found", t))
return
}
info = process(info)
r.WriteJSONData(info)
}
// merge "SupportedResourceTypes" into "SupportedResourceFilters" for UI to render easier
func process(info *adapter.Info) *adapter.Info {
if info == nil {
return nil
}
in := &adapter.Info{
Type: info.Type,
Description: info.Description,
SupportedTriggers: info.SupportedTriggers,
}
filters := []*adapter.Filter{}
for _, filter := range info.SupportedResourceFilters {
if filter.Type != model.FilterTypeResource {
filters = append(filters, filter)
}
}
values := []string{}
for _, resourceType := range info.SupportedResourceTypes {
values = append(values, string(resourceType))
}
filters = append(filters, &adapter.Filter{
Type: model.FilterTypeResource,
Style: adapter.FilterStyleRadio,
Values: values,
})
in.SupportedResourceFilters = filters
return in
}

View File

@ -65,6 +65,7 @@ func TestReplicationAdapterAPIGet(t *testing.T) {
&adapter.Info{
Type: "test",
SupportedResourceTypes: []model.ResourceType{"image"},
SupportedTriggers: []model.TriggerType{"mannual"},
}, fakedFactory)
require.Nil(t, err)

View File

@ -25,17 +25,34 @@ import (
// as the adapter registry
var registry = []*item{}
// const definition
const (
FilterStyleText = "input"
FilterStyleRadio = "radio"
)
// FilterStyle is used for UI to determine how to render the filter
type FilterStyle string
type item struct {
info *Info
factory Factory
}
// Filter ...
type Filter struct {
Type model.FilterType `json:"type"`
Style FilterStyle `json:"style"`
Values []string `json:"values,omitempty"`
}
// Info provides base info and capability declarations of the adapter
type Info struct {
Type model.RegistryType `json:"type"`
Description string `json:"description"`
SupportedResourceTypes []model.ResourceType `json:"supported_resource_types"`
SupportedResourceFilters []model.FilterType `json:"supported_resource_filters"`
SupportedResourceTypes []model.ResourceType `json:"-"`
SupportedResourceFilters []*Filter `json:"supported_resource_filters"`
SupportedTriggers []model.TriggerType `json:"supported_triggers"`
}
// Factory creates a specific Adapter according to the params
@ -63,6 +80,9 @@ func RegisterFactory(info *Info, factory Factory) error {
if len(info.SupportedResourceTypes) == 0 {
return errors.New("must support at least one resource type")
}
if len(info.SupportedTriggers) == 0 {
return errors.New("must support at least one trigger")
}
if factory == nil {
return errors.New("empty adapter factory")
}

View File

@ -34,23 +34,32 @@ func TestRegisterFactory(t *testing.T) {
&Info{
Type: "harbor",
}, nil))
// empty trigger
assert.NotNil(t, RegisterFactory(
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
}, nil))
// empty factory
assert.NotNil(t, RegisterFactory(
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
SupportedTriggers: []model.TriggerType{"mannual"},
}, nil))
// pass
assert.Nil(t, RegisterFactory(
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
SupportedTriggers: []model.TriggerType{"mannual"},
}, fakedFactory))
// already exists
assert.NotNil(t, RegisterFactory(
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
SupportedTriggers: []model.TriggerType{"mannual"},
}, fakedFactory))
}
@ -60,6 +69,7 @@ func TestGetFactory(t *testing.T) {
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
SupportedTriggers: []model.TriggerType{"mannual"},
}, fakedFactory))
// doesn't exist
_, err := GetFactory("gcr")
@ -80,6 +90,7 @@ func TestListAdapterInfos(t *testing.T) {
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
SupportedTriggers: []model.TriggerType{"mannual"},
}, fakedFactory))
infos = ListAdapterInfos()
@ -93,6 +104,7 @@ func TestGetAdapterInfo(t *testing.T) {
&Info{
Type: "harbor",
SupportedResourceTypes: []model.ResourceType{"image"},
SupportedTriggers: []model.TriggerType{"mannual"},
}, fakedFactory))
// doesn't exist

View File

@ -31,11 +31,31 @@ import (
// TODO add UT
func init() {
// TODO add more information to the info
info := &adp.Info{
Type: model.RegistryTypeHarbor,
SupportedResourceTypes: []model.ResourceType{
model.ResourceTypeRepository, model.ResourceTypeChart},
model.ResourceTypeRepository,
model.ResourceTypeChart,
},
SupportedResourceFilters: []*adp.Filter{
{
Type: model.FilterTypeName,
Style: adp.FilterStyleText,
},
{
Type: model.FilterTypeVersion,
Style: adp.FilterStyleText,
},
{
Type: model.FilterTypeLabel,
Style: adp.FilterStyleText,
},
},
SupportedTriggers: []model.TriggerType{
model.TriggerTypeManual,
model.TriggerTypeScheduled,
model.TriggerTypeEventBased,
},
}
// TODO passing coreServiceURL and tokenServiceURL
coreServiceURL := "http://core:8080"

View File

@ -241,6 +241,7 @@ func TestStartReplication(t *testing.T) {
model.ResourceTypeRepository,
model.ResourceTypeChart,
},
SupportedTriggers: []model.TriggerType{model.TriggerTypeManual},
}, fakedAdapterFactory)
require.Nil(t, err)

View File

@ -27,6 +27,10 @@ const (
FilterTypeName = "Name"
FilterTypeVersion = "Version"
FilterTypeLabel = "Label"
TriggerTypeManual = "Manual"
TriggerTypeScheduled = "Scheduled"
TriggerTypeEventBased = "EventBased"
)
// Policy defines the structure of a replication policy

View File

@ -0,0 +1,116 @@
// Copyright 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 chart
import (
"bytes"
"io"
"io/ioutil"
"testing"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/replication/ng/model"
trans "github.com/goharbor/harbor/src/replication/ng/transfer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeRegistry struct{}
func (f *fakeRegistry) FetchCharts(namespaces []string, filters []*model.Filter) ([]*model.Resource, error) {
return []*model.Resource{
{
Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{
Name: "library/harbor",
Namespace: "library",
Vtags: []string{"0.2.0"},
},
},
}, nil
}
func (f *fakeRegistry) ChartExist(name, version string) (bool, error) {
return true, nil
}
func (f *fakeRegistry) DownloadChart(name, version string) (io.ReadCloser, error) {
r := ioutil.NopCloser(bytes.NewReader([]byte{'a'}))
return r, nil
}
func (f *fakeRegistry) UploadChart(name, version string, chart io.Reader) error {
return nil
}
func (f *fakeRegistry) DeleteChart(name, version string) error {
return nil
}
func TestFactory(t *testing.T) {
tr, err := factory(nil, nil)
require.Nil(t, err)
_, ok := tr.(trans.Transfer)
assert.True(t, ok)
}
func TestShouldStop(t *testing.T) {
// should stop
stopFunc := func() bool { return true }
tr := &transfer{
logger: log.DefaultLogger(),
isStopped: stopFunc,
}
assert.True(t, tr.shouldStop())
// should not stop
stopFunc = func() bool { return false }
tr = &transfer{
isStopped: stopFunc,
}
assert.False(t, tr.shouldStop())
}
func TestCopy(t *testing.T) {
stopFunc := func() bool { return false }
transfer := &transfer{
logger: log.DefaultLogger(),
isStopped: stopFunc,
src: &fakeRegistry{},
dst: &fakeRegistry{},
}
src := &chart{
name: "library/harbor",
version: "0.2.0",
}
dst := &chart{
name: "dest/harbor",
version: "0.2.0",
}
err := transfer.copy(src, dst, true)
assert.Nil(t, err)
}
func TestDelete(t *testing.T) {
stopFunc := func() bool { return false }
transfer := &transfer{
logger: log.DefaultLogger(),
isStopped: stopFunc,
src: &fakeRegistry{},
dst: &fakeRegistry{},
}
chart := &chart{
name: "dest/harbor",
version: "0.2.0",
}
err := transfer.delete(chart)
assert.Nil(t, err)
}