mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-02 13:01:23 +01:00
Merge pull request #8032 from ywk253100/190529_filter
Support label filter for replication
This commit is contained in:
commit
8da61464b5
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
247
src/replication/filter/filter.go
Normal file
247
src/replication/filter/filter.go
Normal 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
|
||||
}
|
170
src/replication/filter/filter_test.go
Normal file
170
src/replication/filter/filter_test.go
Normal 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]))
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -29,6 +29,7 @@ const (
|
||||
|
||||
FilterStyleTypeText = "input"
|
||||
FilterStyleTypeRadio = "radio"
|
||||
FilterStyleTypeList = "list"
|
||||
)
|
||||
|
||||
// RegistryType indicates the type of registry
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user