diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b0dfc83ee..96fa2bb19 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/src/core/api/replication_adapter.go b/src/core/api/replication_adapter.go index 1882d5df5..fac813def 100644 --- a/src/core/api/replication_adapter.go +++ b/src/core/api/replication_adapter.go @@ -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 +} diff --git a/src/core/api/replication_adapter_test.go b/src/core/api/replication_adapter_test.go index e405db0a1..7b17be877 100644 --- a/src/core/api/replication_adapter_test.go +++ b/src/core/api/replication_adapter_test.go @@ -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) diff --git a/src/replication/ng/adapter/adapter.go b/src/replication/ng/adapter/adapter.go index 3e33e1f5b..829f75813 100644 --- a/src/replication/ng/adapter/adapter.go +++ b/src/replication/ng/adapter/adapter.go @@ -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") } diff --git a/src/replication/ng/adapter/adapter_test.go b/src/replication/ng/adapter/adapter_test.go index 3896963d1..da36d0877 100644 --- a/src/replication/ng/adapter/adapter_test.go +++ b/src/replication/ng/adapter/adapter_test.go @@ -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 diff --git a/src/replication/ng/adapter/harbor/adapter.go b/src/replication/ng/adapter/harbor/adapter.go index be368c293..7f5d62d91 100644 --- a/src/replication/ng/adapter/harbor/adapter.go +++ b/src/replication/ng/adapter/harbor/adapter.go @@ -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" diff --git a/src/replication/ng/flow/controller_test.go b/src/replication/ng/flow/controller_test.go index a473998b4..6f20391a7 100644 --- a/src/replication/ng/flow/controller_test.go +++ b/src/replication/ng/flow/controller_test.go @@ -241,6 +241,7 @@ func TestStartReplication(t *testing.T) { model.ResourceTypeRepository, model.ResourceTypeChart, }, + SupportedTriggers: []model.TriggerType{model.TriggerTypeManual}, }, fakedAdapterFactory) require.Nil(t, err) diff --git a/src/replication/ng/model/policy.go b/src/replication/ng/model/policy.go index 232441319..d2ea36887 100644 --- a/src/replication/ng/model/policy.go +++ b/src/replication/ng/model/policy.go @@ -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 diff --git a/src/replication/ng/transfer/chart/transfer_test.go b/src/replication/ng/transfer/chart/transfer_test.go new file mode 100644 index 000000000..961dc01a8 --- /dev/null +++ b/src/replication/ng/transfer/chart/transfer_test.go @@ -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) +}