Support label filter for replication

Only the system level labels are supported to be used as the replication policy filter

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2019-06-04 13:40:36 +08:00
parent 592e40bacf
commit 684df243b2
11 changed files with 608 additions and 298 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ const (
FilterStyleTypeText = "input"
FilterStyleTypeRadio = "radio"
FilterStyleTypeList = "list"
)
// RegistryType indicates the type of registry

View File

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