diff --git a/src/replication/adapter/harbor/adapter.go b/src/replication/adapter/harbor/adapter.go index 25fc8e5b5..57ed33045 100644 --- a/src/replication/adapter/harbor/adapter.go +++ b/src/replication/adapter/harbor/adapter.go @@ -97,11 +97,6 @@ func (a *adapter) Info() (*model.RegistryInfo, error) { Type: model.FilterTypeTag, Style: model.FilterStyleTypeText, }, - // TODO add support for label filter - // { - // Type: model.FilterTypeLabel, - // Style: model.FilterStyleTypeText, - // }, }, SupportedTriggers: []model.TriggerType{ model.TriggerTypeManual, @@ -118,6 +113,26 @@ func (a *adapter) Info() (*model.RegistryInfo, error) { if sys.ChartRegistryEnabled { info.SupportedResourceTypes = append(info.SupportedResourceTypes, model.ResourceTypeChart) } + labels := []*struct { + Name string `json:"name"` + }{} + // label isn't supported in some previous version of Harbor + if err := a.client.Get(a.getURL()+"/api/labels?scope=g", &labels); err != nil { + if e, ok := err.(*common_http.Error); !ok || e.Code != http.StatusNotFound { + return nil, err + } + } else { + ls := []string{} + for _, label := range labels { + ls = append(ls, label.Name) + } + labelFilter := &model.FilterStyle{ + Type: model.FilterTypeLabel, + Style: model.FilterStyleTypeList, + Values: ls, + } + info.SupportedResourceFilters = append(info.SupportedResourceFilters, labelFilter) + } return info, nil } @@ -244,12 +259,15 @@ func (a *adapter) getProject(name string) (*project, error) { return nil, nil } -func (a *adapter) getRepositories(projectID int64) ([]*repository, error) { - repositories := []*repository{} +func (a *adapter) getRepositories(projectID int64) ([]*adp.Repository, error) { + repositories := []*adp.Repository{} url := fmt.Sprintf("%s/api/repositories?project_id=%d&page=1&page_size=500", a.getURL(), projectID) if err := a.client.GetAndIteratePagination(url, &repositories); err != nil { return nil, err } + for _, repository := range repositories { + repository.ResourceType = string(model.ResourceTypeImage) + } return repositories, nil } diff --git a/src/replication/adapter/harbor/chart_registry.go b/src/replication/adapter/harbor/chart_registry.go index faa9749c5..81856ced3 100644 --- a/src/replication/adapter/harbor/chart_registry.go +++ b/src/replication/adapter/harbor/chart_registry.go @@ -24,45 +24,17 @@ import ( "strings" common_http "github.com/goharbor/harbor/src/common/http" + adp "github.com/goharbor/harbor/src/replication/adapter" "github.com/goharbor/harbor/src/replication/model" ) -type chart struct { - Name string `json:"name"` - Project string -} - -func (c *chart) Match(filters []*model.Filter) (bool, error) { - supportedFilters := []*model.Filter{} - for _, filter := range filters { - if filter.Type == model.FilterTypeName { - supportedFilters = append(supportedFilters, filter) - } - } - item := &FilterItem{ - Value: fmt.Sprintf("%s/%s", c.Project, c.Name), - } - return item.Match(supportedFilters) +type label struct { + Name string `json:"name"` } type chartVersion struct { - Name string `json:"name"` - Version string `json:"version"` - // TODO handle system/project level labels - // Labels string `json:"labels"` -} - -func (c *chartVersion) Match(filters []*model.Filter) (bool, error) { - supportedFilters := []*model.Filter{} - for _, filter := range filters { - if filter.Type == model.FilterTypeTag { - supportedFilters = append(supportedFilters, filter) - } - } - item := &FilterItem{ - Value: c.Version, - } - return item.Match(supportedFilters) + Version string `json:"version"` + Labels []*label `json:"labels"` } type chartVersionDetail struct { @@ -81,37 +53,60 @@ func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error resources := []*model.Resource{} for _, project := range projects { url := fmt.Sprintf("%s/api/chartrepo/%s/charts", a.getURL(), project.Name) - charts := []*chart{} - if err := a.client.Get(url, &charts); err != nil { + repositories := []*adp.Repository{} + if err := a.client.Get(url, &repositories); err != nil { return nil, err } - for _, chart := range charts { - chart.Project = project.Name + if len(repositories) == 0 { + continue } - charts, err := filterCharts(charts, filters) - if err != nil { - return nil, err + for _, repository := range repositories { + repository.Name = fmt.Sprintf("%s/%s", project.Name, repository.Name) + repository.ResourceType = string(model.ResourceTypeChart) } - for _, chart := range charts { - url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s", a.getURL(), project.Name, chart.Name) - chartVersions := []*chartVersion{} - if err := a.client.Get(url, &chartVersions); err != nil { + for _, filter := range filters { + if err = filter.DoFilter(&repositories); err != nil { return nil, err } - chartVersions, err = filterChartVersions(chartVersions, filters) - if err != nil { + } + for _, repository := range repositories { + name := strings.SplitN(repository.Name, "/", 2)[1] + url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s", a.getURL(), project.Name, name) + versions := []*chartVersion{} + if err := a.client.Get(url, &versions); err != nil { return nil, err } - for _, version := range chartVersions { + if len(versions) == 0 { + continue + } + vTags := []*adp.VTag{} + for _, version := range versions { + var labels []string + for _, label := range version.Labels { + labels = append(labels, label.Name) + } + vTags = append(vTags, &adp.VTag{ + Name: version.Version, + Labels: labels, + ResourceType: string(model.ResourceTypeChart), + }) + } + for _, filter := range filters { + if err = filter.DoFilter(&vTags); err != nil { + return nil, err + } + } + + for _, vTag := range vTags { resources = append(resources, &model.Resource{ Type: model.ResourceTypeChart, Registry: a.registry, Metadata: &model.ResourceMetadata{ Repository: &model.Repository{ - Name: fmt.Sprintf("%s/%s", project.Name, chart.Name), + Name: repository.Name, Metadata: project.Metadata, }, - Vtags: []string{version.Version}, + Vtags: []string{vTag.Name}, }, }) } @@ -232,31 +227,3 @@ func parseChartName(name string) (string, string, error) { } return "", "", fmt.Errorf("invalid chart name format: %s", name) } - -func filterCharts(charts []*chart, filters []*model.Filter) ([]*chart, error) { - result := []*chart{} - for _, chart := range charts { - match, err := chart.Match(filters) - if err != nil { - return nil, err - } - if match { - result = append(result, chart) - } - } - return result, nil -} - -func filterChartVersions(chartVersions []*chartVersion, filters []*model.Filter) ([]*chartVersion, error) { - result := []*chartVersion{} - for _, chartVersion := range chartVersions { - match, err := chartVersion.Match(filters) - if err != nil { - return nil, err - } - if match { - result = append(result, chartVersion) - } - } - return result, nil -} diff --git a/src/replication/adapter/harbor/filter.go b/src/replication/adapter/harbor/filter.go deleted file mode 100644 index c7f7b910c..000000000 --- a/src/replication/adapter/harbor/filter.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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 harbor - -import ( - "fmt" - - "github.com/goharbor/harbor/src/replication/model" - "github.com/goharbor/harbor/src/replication/util" -) - -// TODO unify the filter logic from different adapters into one? -// and move the code into a separated common package - -// Filterable defines the interface that an object should implement -// if the object can be filtered -type Filterable interface { - Match([]*model.Filter) (bool, error) -} - -// FilterItem is a filterable object that can be used to match string pattern -type FilterItem struct { - Value string -} - -// Match ... -func (f *FilterItem) Match(filters []*model.Filter) (bool, error) { - if len(filters) == 0 { - return true, nil - } - matched := true - for _, filter := range filters { - pattern, ok := filter.Value.(string) - if !ok { - return false, fmt.Errorf("the type of filter value isn't string: %v", filter) - } - m, err := util.Match(pattern, f.Value) - if err != nil { - return false, err - } - if !m { - matched = false - break - } - } - return matched, nil -} diff --git a/src/replication/adapter/harbor/filter_test.go b/src/replication/adapter/harbor/filter_test.go deleted file mode 100644 index 8e873c17b..000000000 --- a/src/replication/adapter/harbor/filter_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// 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 harbor - -import ( - "testing" - - "github.com/goharbor/harbor/src/replication/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMatch(t *testing.T) { - // nil filters - item := &FilterItem{} - match, err := item.Match(nil) - require.Nil(t, err) - assert.True(t, match) - // contains filter whose value isn't string - item = &FilterItem{} - filters := []*model.Filter{ - { - Type: "test", - Value: 1, - }, - } - match, err = item.Match(filters) - require.NotNil(t, err) - // both filters match - item = &FilterItem{ - Value: "b/c", - } - filters = []*model.Filter{ - { - Value: "b/*", - }, - { - Value: "*/c", - }, - } - match, err = item.Match(filters) - require.Nil(t, err) - assert.True(t, match) - // one filter matches and the other one doesn't - item = &FilterItem{ - Value: "b/c", - } - filters = []*model.Filter{ - { - Value: "b/*", - }, - { - Value: "d", - }, - } - match, err = item.Match(filters) - require.Nil(t, err) - assert.False(t, match) - // both filters don't match - item = &FilterItem{ - Value: "b/c", - } - filters = []*model.Filter{ - { - Value: "f", - }, - { - Value: "d", - }, - } - match, err = item.Match(filters) - require.Nil(t, err) - assert.False(t, match) -} diff --git a/src/replication/adapter/harbor/image_registry.go b/src/replication/adapter/harbor/image_registry.go index c64c0c882..a278bb099 100644 --- a/src/replication/adapter/harbor/image_registry.go +++ b/src/replication/adapter/harbor/image_registry.go @@ -19,44 +19,11 @@ import ( "strings" "github.com/goharbor/harbor/src/common/utils/log" + adp "github.com/goharbor/harbor/src/replication/adapter" "github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/util" ) -type repository struct { - Name string `json:"name"` -} - -func (r *repository) Match(filters []*model.Filter) (bool, error) { - supportedFilters := []*model.Filter{} - for _, filter := range filters { - if filter.Type == model.FilterTypeName { - supportedFilters = append(supportedFilters, filter) - } - } - item := &FilterItem{ - Value: r.Name, - } - return item.Match(supportedFilters) -} - -type tag struct { - Name string `json:"name"` -} - -func (t *tag) Match(filters []*model.Filter) (bool, error) { - supportedFilters := []*model.Filter{} - for _, filter := range filters { - if filter.Type == model.FilterTypeTag { - supportedFilters = append(supportedFilters, filter) - } - } - item := &FilterItem{ - Value: t.Name, - } - return item.Match(supportedFilters) -} - func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error) { projects, err := a.listCandidateProjects(filters) if err != nil { @@ -68,26 +35,33 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error if err != nil { return nil, err } - repositories, err = filterRepositories(repositories, filters) - if err != nil { - return nil, err + if len(repositories) == 0 { + continue } - for _, repository := range repositories { - url := fmt.Sprintf("%s/api/repositories/%s/tags", a.getURL(), repository.Name) - tags := []*tag{} - if err = a.client.Get(url, &tags); err != nil { + for _, filter := range filters { + if err = filter.DoFilter(&repositories); err != nil { return nil, err } - tags, err = filterTags(tags, filters) + } + for _, repository := range repositories { + vTags, err := a.getTags(repository.Name) if err != nil { return nil, err } - if len(tags) == 0 { + if len(vTags) == 0 { continue } - vtags := []string{} - for _, tag := range tags { - vtags = append(vtags, tag.Name) + for _, filter := range filters { + if err = filter.DoFilter(&vTags); err != nil { + return nil, err + } + } + if len(vTags) == 0 { + continue + } + tags := []string{} + for _, vTag := range vTags { + tags = append(tags, vTag.Name) } resources = append(resources, &model.Resource{ Type: model.ResourceTypeImage, @@ -97,7 +71,7 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error Name: repository.Name, Metadata: project.Metadata, }, - Vtags: vtags, + Vtags: tags, }, }) } @@ -150,30 +124,28 @@ func (a *adapter) DeleteManifest(repository, reference string) error { return a.client.Delete(url) } -func filterRepositories(repositories []*repository, filters []*model.Filter) ([]*repository, error) { - result := []*repository{} - for _, repository := range repositories { - match, err := repository.Match(filters) - if err != nil { - return nil, err - } - if match { - result = append(result, repository) +func (a *adapter) getTags(repository string) ([]*adp.VTag, error) { + url := fmt.Sprintf("%s/api/repositories/%s/tags", a.getURL(), repository) + tags := []*struct { + Name string `json:"name"` + Labels []*struct { + Name string `json:"name"` } + }{} + if err := a.client.Get(url, &tags); err != nil { + return nil, err } - return result, nil -} - -func filterTags(tags []*tag, filters []*model.Filter) ([]*tag, error) { - result := []*tag{} + vTags := []*adp.VTag{} for _, tag := range tags { - match, err := tag.Match(filters) - if err != nil { - return nil, err - } - if match { - result = append(result, tag) + var labels []string + for _, label := range tag.Labels { + labels = append(labels, label.Name) } + vTags = append(vTags, &adp.VTag{ + Name: tag.Name, + Labels: labels, + ResourceType: string(model.ResourceTypeImage), + }) } - return result, nil + return vTags, nil } diff --git a/src/replication/adapter/image_registry.go b/src/replication/adapter/image_registry.go index fa4122fdc..3249168d8 100644 --- a/src/replication/adapter/image_registry.go +++ b/src/replication/adapter/image_registry.go @@ -21,6 +21,8 @@ import ( "strings" "sync" + "github.com/goharbor/harbor/src/replication/filter" + "github.com/docker/distribution" "github.com/docker/distribution/manifest/schema1" "github.com/goharbor/harbor/src/common/http/modifier" @@ -50,6 +52,59 @@ type ImageRegistry interface { PushBlob(repository, digest string, size int64, blob io.Reader) error } +// Repository defines an repository object, it can be image repository, chart repository and etc. +type Repository struct { + ResourceType string `json:"resource_type"` + Name string `json:"name"` +} + +// GetName returns the name +func (r *Repository) GetName() string { + return r.Name +} + +// GetFilterableType returns the filterable type +func (r *Repository) GetFilterableType() filter.FilterableType { + return filter.FilterableTypeRepository +} + +// GetResourceType returns the resource type +func (r *Repository) GetResourceType() string { + return r.ResourceType +} + +// GetLabels returns the labels +func (r *Repository) GetLabels() []string { + return nil +} + +// VTag defines an vTag object, it can be image tag, chart version and etc. +type VTag struct { + ResourceType string `json:"resource_type"` + Name string `json:"name"` + Labels []string `json:"labels"` +} + +// GetFilterableType returns the filterable type +func (v *VTag) GetFilterableType() filter.FilterableType { + return filter.FilterableTypeVTag +} + +// GetResourceType returns the resource type +func (v *VTag) GetResourceType() string { + return v.ResourceType +} + +// GetName returns the name +func (v *VTag) GetName() string { + return v.Name +} + +// GetLabels returns the labels +func (v *VTag) GetLabels() []string { + return v.Labels +} + // DefaultImageRegistry provides a default implementation for interface ImageRegistry type DefaultImageRegistry struct { sync.RWMutex diff --git a/src/replication/filter/filter.go b/src/replication/filter/filter.go new file mode 100644 index 000000000..6f2d6e72a --- /dev/null +++ b/src/replication/filter/filter.go @@ -0,0 +1,247 @@ +// 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 filter + +import ( + "errors" + "reflect" + + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/replication/util" +) + +// const definitions +const ( + FilterableTypeRepository = "repository" + FilterableTypeVTag = "vtag" +) + +// FilterableType specifies the type of the filterable +type FilterableType string + +// Filterable defines the methods that a filterable object must implement +type Filterable interface { + // return what the type of the filterable object is(repository or vtag) + GetFilterableType() FilterableType + // return the resource type of the filterable object(image, chart, ...) + GetResourceType() string + GetName() string + GetLabels() []string +} + +// Filter defines the methods that a filter must implement +type Filter interface { + // return whether the filter is applied to the specified Filterable + ApplyTo(Filterable) bool + Filter(...Filterable) ([]Filterable, error) +} + +// NewResourceTypeFilter return a Filter to filter candidates according to the resource type +func NewResourceTypeFilter(resourceType string) Filter { + return &resourceTypeFilter{ + resourceType: resourceType, + } +} + +// NewRepositoryNameFilter return a Filter to filter the repositories according to the name +func NewRepositoryNameFilter(pattern string) Filter { + return &nameFilter{ + filterableType: FilterableTypeRepository, + pattern: pattern, + } +} + +// NewVTagNameFilter return a Filter to filter the vtags according to the name +func NewVTagNameFilter(pattern string) Filter { + return &nameFilter{ + filterableType: FilterableTypeVTag, + pattern: pattern, + } +} + +// NewVTagLabelFilter return a Filter to filter vtags according to the label +func NewVTagLabelFilter(label string) Filter { + return &labelFilter{ + label: label, + } +} + +type resourceTypeFilter struct { + resourceType string +} + +func (r *resourceTypeFilter) ApplyTo(filterable Filterable) bool { + if filterable == nil { + return false + } + switch filterable.GetFilterableType() { + case FilterableTypeRepository, FilterableTypeVTag: + return true + default: + return false + } +} + +func (r *resourceTypeFilter) Filter(filterables ...Filterable) ([]Filterable, error) { + result := []Filterable{} + for _, filterable := range filterables { + if filterable.GetResourceType() == r.resourceType { + result = append(result, filterable) + } + } + return result, nil +} + +type nameFilter struct { + filterableType FilterableType + pattern string +} + +func (n *nameFilter) ApplyTo(filterable Filterable) bool { + if filterable == nil { + return false + } + if filterable.GetFilterableType() == n.filterableType { + return true + } + return false +} + +func (n *nameFilter) Filter(filterables ...Filterable) ([]Filterable, error) { + result := []Filterable{} + for _, filterable := range filterables { + name := filterable.GetName() + match, err := util.Match(n.pattern, name) + if err != nil { + return nil, err + } + if match { + log.Debugf("%q matches the pattern %q of name filter", name, n.pattern) + result = append(result, filterable) + continue + } + log.Debugf("%q doesn't match the pattern %q of name filter, skip", name, n.pattern) + } + return result, nil +} + +type labelFilter struct { + label string +} + +func (l *labelFilter) ApplyTo(filterable Filterable) bool { + if filterable == nil { + return false + } + if filterable.GetFilterableType() == FilterableTypeVTag { + return true + } + return false +} + +func (l *labelFilter) Filter(filterables ...Filterable) ([]Filterable, error) { + // if no specified label in the filter, just returns the input filterable + // candidate as the result + if len(l.label) == 0 { + return filterables, nil + } + result := []Filterable{} + for _, filterable := range filterables { + match := false + for _, label := range filterable.GetLabels() { + if label == l.label { + match = true + break + } + } + if match { + result = append(result, filterable) + } + } + return result, nil +} + +// DoFilter is a util function to help filter filterables easily. +// The parameter "filterables" must be a pointer points to a slice +// whose elements must be Filterable. After applying all the "filters" +// to the "filterables", the result is put back into the variable +// "filterables" +func DoFilter(filterables interface{}, filters ...Filter) error { + if filterables == nil || len(filters) == 0 { + return nil + } + + value := reflect.ValueOf(filterables) + // make sure the input is a pointer + if value.Kind() != reflect.Ptr { + return errors.New("the type of input should be pointer to a Filterable slice") + } + + sliceValue := value.Elem() + // make sure the input is a pointer points to a slice + if sliceValue.Type().Kind() != reflect.Slice { + return errors.New("the type of input should be pointer to a Filterable slice") + } + + filterableType := reflect.TypeOf((*Filterable)(nil)).Elem() + elemType := sliceValue.Type().Elem() + // make sure the input is a pointer points to a Filterable slice + if !elemType.Implements(filterableType) { + return errors.New("the type of input should be pointer to a Filterable slice") + } + + // convert the input to Filterable slice + items := []Filterable{} + for i := 0; i < sliceValue.Len(); i++ { + items = append(items, sliceValue.Index(i).Interface().(Filterable)) + } + + // do filter + var err error + items, err = doFilter(items, filters...) + if err != nil { + return err + } + + // convert back to the origin type + result := reflect.MakeSlice(reflect.SliceOf(elemType), 0, len(items)) + for _, item := range items { + result = reflect.Append(result, reflect.ValueOf(item)) + } + value.Elem().Set(result) + + return nil +} + +func doFilter(filterables []Filterable, filters ...Filter) ([]Filterable, error) { + var appliedTo, notAppliedTo []Filterable + var err error + for _, filter := range filters { + appliedTo, notAppliedTo = nil, nil + for _, filterable := range filterables { + if filter.ApplyTo(filterable) { + appliedTo = append(appliedTo, filterable) + } else { + notAppliedTo = append(notAppliedTo, filterable) + } + } + filterables, err = filter.Filter(appliedTo...) + if err != nil { + return nil, err + } + filterables = append(filterables, notAppliedTo...) + } + return filterables, nil +} diff --git a/src/replication/filter/filter_test.go b/src/replication/filter/filter_test.go new file mode 100644 index 000000000..4a379c048 --- /dev/null +++ b/src/replication/filter/filter_test.go @@ -0,0 +1,170 @@ +// 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 filter + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type fakeFilterable struct { + filterableType FilterableType + resourceType string + name string + labels []string +} + +func (f *fakeFilterable) GetFilterableType() FilterableType { + return f.filterableType +} + +func (f *fakeFilterable) GetResourceType() string { + return f.resourceType +} + +func (f *fakeFilterable) GetName() string { + return f.name +} +func (f *fakeFilterable) GetLabels() []string { + return f.labels +} + +func TestFilterOfResourceTypeFilter(t *testing.T) { + filterable := &fakeFilterable{ + filterableType: FilterableTypeRepository, + resourceType: "image", + name: "library/hello-world", + } + + filter := NewResourceTypeFilter("image") + result, err := filter.Filter(filterable) + require.Nil(t, nil, err) + if assert.Equal(t, 1, len(result)) { + assert.True(t, reflect.DeepEqual(filterable, result[0])) + } + + filter = NewResourceTypeFilter("chart") + result, err = filter.Filter(filterable) + require.Nil(t, nil, err) + assert.Equal(t, 0, len(result)) +} + +func TestApplyToOfResourceTypeFilter(t *testing.T) { + filterable := &fakeFilterable{ + filterableType: FilterableTypeRepository, + } + + filter := NewResourceTypeFilter("image") + assert.True(t, filter.ApplyTo(filterable)) + + filterable.filterableType = FilterableTypeVTag + assert.True(t, filter.ApplyTo(filterable)) + + filterable.filterableType = FilterableType("unknown") + assert.False(t, filter.ApplyTo(filterable)) +} + +func TestFilterOfNameFilter(t *testing.T) { + filterable := &fakeFilterable{ + name: "foo", + } + // pass the filter + filter := &nameFilter{ + pattern: "*", + } + result, err := filter.Filter(filterable) + require.Nil(t, err) + if assert.Equal(t, 1, len(result)) { + assert.True(t, reflect.DeepEqual(filterable, result[0].(*fakeFilterable))) + } + + // cannot pass the filter + filter.pattern = "cannotpass" + result, err = filter.Filter(filterable) + require.Nil(t, err) + assert.Equal(t, 0, len(result)) +} + +func TestApplyToOfNameFilter(t *testing.T) { + filterable := &fakeFilterable{ + filterableType: FilterableTypeRepository, + } + + filter := &nameFilter{ + filterableType: FilterableTypeRepository, + } + assert.True(t, filter.ApplyTo(filterable)) + + filterable.filterableType = FilterableTypeVTag + assert.False(t, filter.ApplyTo(filterable)) +} + +func TestFilterOfLabelFilter(t *testing.T) { + filterable := &fakeFilterable{ + labels: []string{"production"}, + } + // pass the filter + filter := &labelFilter{ + label: "production", + } + result, err := filter.Filter(filterable) + require.Nil(t, err) + if assert.Equal(t, 1, len(result)) { + assert.True(t, reflect.DeepEqual(filterable, result[0].(*fakeFilterable))) + } + // cannot pass the filter + filter.label = "cannotpass" + result, err = filter.Filter(filterable) + require.Nil(t, err) + assert.Equal(t, 0, len(result)) +} + +func TestApplyToOfLabelFilter(t *testing.T) { + filterable := &fakeFilterable{ + filterableType: FilterableTypeRepository, + } + + filter := labelFilter{} + assert.False(t, filter.ApplyTo(filterable)) + + filterable.filterableType = FilterableTypeVTag + assert.True(t, filter.ApplyTo(filterable)) +} + +func TestDoFilter(t *testing.T) { + tag1 := &fakeFilterable{ + filterableType: FilterableTypeVTag, + name: "1.0", + labels: []string{"production"}, + } + tag2 := &fakeFilterable{ + filterableType: FilterableTypeVTag, + name: "latest", + labels: []string{"dev"}, + } + filterables := []Filterable{tag1, tag2} + filters := []Filter{ + NewVTagNameFilter("*"), + NewVTagLabelFilter("production"), + } + err := DoFilter(&filterables, filters...) + require.Nil(t, err) + if assert.Equal(t, 1, len(filterables)) { + assert.True(t, reflect.DeepEqual(tag1, filterables[0])) + } +} diff --git a/src/replication/model/policy.go b/src/replication/model/policy.go index 16b8715ed..130f34e3c 100644 --- a/src/replication/model/policy.go +++ b/src/replication/model/policy.go @@ -18,6 +18,8 @@ import ( "fmt" "time" + "github.com/goharbor/harbor/src/replication/filter" + "github.com/astaxie/beego/validation" "github.com/goharbor/harbor/src/common/models" "github.com/robfig/cron" @@ -133,6 +135,29 @@ type Filter struct { Value interface{} `json:"value"` } +// DoFilter filter the filterables +// The parameter "filterables" must be a pointer points to a slice +// whose elements must be Filterable. After applying the filter +// to the "filterables", the result is put back into the variable +// "filterables" +func (f *Filter) DoFilter(filterables interface{}) error { + var ft filter.Filter + switch f.Type { + case FilterTypeName: + ft = filter.NewRepositoryNameFilter(f.Value.(string)) + case FilterTypeTag: + ft = filter.NewVTagNameFilter(f.Value.(string)) + case FilterTypeLabel: + ft = filter.NewVTagLabelFilter(f.Value.(string)) + case FilterTypeResource: + ft = filter.NewResourceTypeFilter(f.Value.(string)) + default: + return fmt.Errorf("unsupported filter type: %s", f.Type) + } + + return filter.DoFilter(filterables, ft) +} + // TriggerType represents the type of trigger. type TriggerType string diff --git a/src/replication/model/registry.go b/src/replication/model/registry.go index b8b074aed..515f7dce2 100644 --- a/src/replication/model/registry.go +++ b/src/replication/model/registry.go @@ -29,6 +29,7 @@ const ( FilterStyleTypeText = "input" FilterStyleTypeRadio = "radio" + FilterStyleTypeList = "list" ) // RegistryType indicates the type of registry diff --git a/src/replication/policy/manager/manager.go b/src/replication/policy/manager/manager.go index 4cbf1f404..42735e9ee 100644 --- a/src/replication/policy/manager/manager.go +++ b/src/replication/policy/manager/manager.go @@ -258,7 +258,7 @@ func parseFilters(str string) ([]*model.Filter, error) { case "tag": filter.Type = model.FilterTypeTag case "label": - // TODO if we support the label filter, remove the checking logic here + // drop all legend label filters continue default: log.Warningf("unknown filter type: %s", filter.Type)