mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 18:55:18 +01:00
Merge pull request #7498 from ywk253100/190423_docker_hub
Support replicate public repositories from Docker Hub
This commit is contained in:
commit
7160e411cc
@ -38,12 +38,7 @@ func NewBasicAuthCredential(username, password string) Credential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *basicAuthCredential) AddAuthorization(req *http.Request) {
|
func (b *basicAuthCredential) AddAuthorization(req *http.Request) {
|
||||||
// only add the authentication info when the username isn't empty
|
req.SetBasicAuth(b.username, b.password)
|
||||||
// the logic is needed for requesting resources from docker hub's
|
|
||||||
// public repositories
|
|
||||||
if len(b.username) > 0 {
|
|
||||||
req.SetBasicAuth(b.username, b.password)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// implement github.com/goharbor/harbor/src/common/http/modifier.Modifier
|
// implement github.com/goharbor/harbor/src/common/http/modifier.Modifier
|
||||||
|
@ -10,40 +10,60 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
||||||
adp "github.com/goharbor/harbor/src/replication/adapter"
|
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
"github.com/goharbor/harbor/src/replication/util"
|
"github.com/goharbor/harbor/src/replication/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := adp.RegisterFactory(model.RegistryTypeDockerHub, func(registry *model.Registry) (adp.Adapter, error) {
|
if err := adp.RegisterFactory(model.RegistryTypeDockerHub, factory); err != nil {
|
||||||
registry.URL = baseURL
|
|
||||||
client, err := NewClient(registry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
reg, err := adp.NewDefaultImageRegistry(&model.Registry{
|
|
||||||
Name: registry.Name,
|
|
||||||
URL: registryURL,
|
|
||||||
Credential: registry.Credential,
|
|
||||||
Insecure: registry.Insecure,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &adapter{
|
|
||||||
client: client,
|
|
||||||
registry: registry,
|
|
||||||
DefaultImageRegistry: reg,
|
|
||||||
}, nil
|
|
||||||
}); err != nil {
|
|
||||||
log.Errorf("Register adapter factory for %s error: %v", model.RegistryTypeDockerHub, err)
|
log.Errorf("Register adapter factory for %s error: %v", model.RegistryTypeDockerHub, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infof("Factory for adapter %s registered", model.RegistryTypeDockerHub)
|
log.Infof("Factory for adapter %s registered", model.RegistryTypeDockerHub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func factory(registry *model.Registry) (adp.Adapter, error) {
|
||||||
|
client, err := NewClient(&model.Registry{
|
||||||
|
URL: baseURL, // specify the URL of Docker Hub
|
||||||
|
Credential: registry.Credential,
|
||||||
|
Insecure: registry.Insecure,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the registry.Credentail isn't specified, the credential here is nil
|
||||||
|
// the client will request the token with no authentication
|
||||||
|
// this is needed for pulling images from public repositories
|
||||||
|
var credential auth.Credential
|
||||||
|
if registry.Credential != nil && len(registry.Credential.AccessSecret) != 0 {
|
||||||
|
credential = auth.NewBasicAuthCredential(
|
||||||
|
registry.Credential.AccessKey,
|
||||||
|
registry.Credential.AccessSecret)
|
||||||
|
}
|
||||||
|
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
|
||||||
|
Transport: util.GetHTTPTransport(registry.Insecure),
|
||||||
|
}, credential)
|
||||||
|
|
||||||
|
reg, err := adp.NewDefaultImageRegistryWithCustomizedAuthorizer(&model.Registry{
|
||||||
|
Name: registry.Name,
|
||||||
|
URL: registryURL, // specify the URL of Docker Hub registry service
|
||||||
|
Credential: registry.Credential,
|
||||||
|
Insecure: registry.Insecure,
|
||||||
|
}, authorizer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &adapter{
|
||||||
|
client: client,
|
||||||
|
registry: registry,
|
||||||
|
DefaultImageRegistry: reg,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type adapter struct {
|
type adapter struct {
|
||||||
*adp.DefaultImageRegistry
|
*adp.DefaultImageRegistry
|
||||||
registry *model.Registry
|
registry *model.Registry
|
||||||
@ -111,8 +131,7 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListNamespaces lists namespaces from DockerHub with the provided query conditions.
|
func (a *adapter) listNamespaces() ([]string, error) {
|
||||||
func (a *adapter) ListNamespaces(query *model.NamespaceQuery) ([]*model.Namespace, error) {
|
|
||||||
resp, err := a.client.Do(http.MethodGet, listNamespacePath, nil)
|
resp, err := a.client.Do(http.MethodGet, listNamespacePath, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -134,18 +153,8 @@ func (a *adapter) ListNamespaces(query *model.NamespaceQuery) ([]*model.Namespac
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var result []*model.Namespace
|
log.Debugf("got namespaces %v by calling the listing namespaces API", namespaces)
|
||||||
for _, ns := range namespaces.Namespaces {
|
return namespaces.Namespaces, nil
|
||||||
// If query set, skip the namespace that doesn't match the query.
|
|
||||||
if query != nil && len(query.Name) > 0 && strings.Index(ns, query.Name) != -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, &model.Namespace{
|
|
||||||
Name: ns,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateNamespace creates a new namespace in DockerHub
|
// CreateNamespace creates a new namespace in DockerHub
|
||||||
@ -229,7 +238,7 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
namespaces, err := a.ListNamespaces(nil)
|
namespaces, err := a.listCandidateNamespaces(nameFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -239,7 +248,7 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
|
|||||||
pageSize := 100
|
pageSize := 100
|
||||||
n := 0
|
n := 0
|
||||||
for {
|
for {
|
||||||
pageRepos, err := a.getRepos(ns.Name, "", page, pageSize)
|
pageRepos, err := a.getRepos(ns, "", page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get repos for namespace '%s' from DockerHub error: %v", ns, err)
|
return nil, fmt.Errorf("get repos for namespace '%s' from DockerHub error: %v", ns, err)
|
||||||
}
|
}
|
||||||
@ -252,7 +261,7 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
|
|||||||
|
|
||||||
page++
|
page++
|
||||||
}
|
}
|
||||||
log.Debugf("got %d repositories for namespace %s", n, ns.Name)
|
log.Debugf("got %d repositories for namespace %s", n, ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resources []*model.Resource
|
var resources []*model.Resource
|
||||||
@ -319,6 +328,22 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
|
|||||||
return resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *adapter) listCandidateNamespaces(pattern string) ([]string, error) {
|
||||||
|
namespaces := []string{}
|
||||||
|
if len(pattern) > 0 {
|
||||||
|
substrings := strings.Split(pattern, "/")
|
||||||
|
namespacePattern := substrings[0]
|
||||||
|
if nms, ok := util.IsSpecificPathComponent(namespacePattern); ok {
|
||||||
|
namespaces = append(namespaces, nms...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(namespaces) > 0 {
|
||||||
|
log.Debugf("parsed the namespaces %v from pattern %s", namespaces, pattern)
|
||||||
|
return namespaces, nil
|
||||||
|
}
|
||||||
|
return a.listNamespaces()
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteManifest ...
|
// DeleteManifest ...
|
||||||
// Note: DockerHub only supports delete by tag
|
// Note: DockerHub only supports delete by tag
|
||||||
func (a *adapter) DeleteManifest(repository, reference string) error {
|
func (a *adapter) DeleteManifest(repository, reference string) error {
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO add more unit test
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testUser = ""
|
testUser = ""
|
||||||
testPassword = ""
|
testPassword = ""
|
||||||
@ -42,9 +44,9 @@ func TestListNamespaces(t *testing.T) {
|
|||||||
ad := getAdapter(t)
|
ad := getAdapter(t)
|
||||||
adapter := ad.(*adapter)
|
adapter := ad.(*adapter)
|
||||||
|
|
||||||
namespaces, err := adapter.ListNamespaces(nil)
|
namespaces, err := adapter.listNamespaces()
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
for _, ns := range namespaces {
|
for _, ns := range namespaces {
|
||||||
fmt.Println(ns.Name)
|
fmt.Println(ns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,5 +231,5 @@ func (a *adapter) getProject(name string) (*project, error) {
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("project %s not found", name)
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -76,8 +76,7 @@ type chartVersionMetadata struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error) {
|
func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error) {
|
||||||
// TODO optimize the performance
|
projects, err := a.listCandidateProjects(filters)
|
||||||
projects, err := a.getProjects("")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,11 @@ package harbor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
|
"github.com/goharbor/harbor/src/replication/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type repository struct {
|
type repository struct {
|
||||||
@ -55,8 +58,7 @@ func (t *tag) Match(filters []*model.Filter) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error) {
|
func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error) {
|
||||||
// TODO optimize the performance
|
projects, err := a.listCandidateProjects(filters)
|
||||||
projects, err := a.getProjects("")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -105,6 +107,43 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
|
|||||||
return resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *adapter) listCandidateProjects(filters []*model.Filter) ([]*project, error) {
|
||||||
|
pattern := ""
|
||||||
|
for _, filter := range filters {
|
||||||
|
if filter.Type == model.FilterTypeName {
|
||||||
|
pattern = filter.Value.(string)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
projects := []*project{}
|
||||||
|
if len(pattern) > 0 {
|
||||||
|
substrings := strings.Split(pattern, "/")
|
||||||
|
projectPattern := substrings[0]
|
||||||
|
names, ok := util.IsSpecificPathComponent(projectPattern)
|
||||||
|
if ok {
|
||||||
|
for _, name := range names {
|
||||||
|
project, err := a.getProject(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if project == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
projects = append(projects, project)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(projects) > 0 {
|
||||||
|
names := []string{}
|
||||||
|
for _, project := range projects {
|
||||||
|
names = append(names, project.Name)
|
||||||
|
}
|
||||||
|
log.Debugf("parsed the projects %v from pattern %s", names, pattern)
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
return a.getProjects("")
|
||||||
|
}
|
||||||
|
|
||||||
// override the default implementation from the default image registry
|
// override the default implementation from the default image registry
|
||||||
// by calling Harbor API directly
|
// by calling Harbor API directly
|
||||||
func (a *adapter) DeleteManifest(repository, reference string) error {
|
func (a *adapter) DeleteManifest(repository, reference string) error {
|
||||||
|
@ -62,12 +62,7 @@ type DefaultImageRegistry struct {
|
|||||||
|
|
||||||
// NewDefaultImageRegistry returns an instance of DefaultImageRegistry
|
// NewDefaultImageRegistry returns an instance of DefaultImageRegistry
|
||||||
func NewDefaultImageRegistry(registry *model.Registry) (*DefaultImageRegistry, error) {
|
func NewDefaultImageRegistry(registry *model.Registry) (*DefaultImageRegistry, error) {
|
||||||
transport := util.GetHTTPTransport(registry.Insecure)
|
var authorizer modifier.Modifier
|
||||||
modifiers := []modifier.Modifier{
|
|
||||||
&auth.UserAgentModifier{
|
|
||||||
UserAgent: UserAgentReplication,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if registry.Credential != nil && len(registry.Credential.AccessSecret) != 0 {
|
if registry.Credential != nil && len(registry.Credential.AccessSecret) != 0 {
|
||||||
var cred modifier.Modifier
|
var cred modifier.Modifier
|
||||||
if registry.Credential.Type == model.CredentialTypeSecret {
|
if registry.Credential.Type == model.CredentialTypeSecret {
|
||||||
@ -83,10 +78,22 @@ func NewDefaultImageRegistry(registry *model.Registry) (*DefaultImageRegistry, e
|
|||||||
if len(registry.CoreURL) > 0 {
|
if len(registry.CoreURL) > 0 {
|
||||||
tokenServiceURL = fmt.Sprintf("%s/service/token", registry.CoreURL)
|
tokenServiceURL = fmt.Sprintf("%s/service/token", registry.CoreURL)
|
||||||
}
|
}
|
||||||
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
|
authorizer = auth.NewStandardTokenAuthorizer(&http.Client{
|
||||||
Transport: transport,
|
Transport: util.GetHTTPTransport(registry.Insecure),
|
||||||
}, cred, tokenServiceURL)
|
}, cred, tokenServiceURL)
|
||||||
|
}
|
||||||
|
return NewDefaultImageRegistryWithCustomizedAuthorizer(registry, authorizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultImageRegistryWithCustomizedAuthorizer returns an instance of DefaultImageRegistry with the customized authorizer
|
||||||
|
func NewDefaultImageRegistryWithCustomizedAuthorizer(registry *model.Registry, authorizer modifier.Modifier) (*DefaultImageRegistry, error) {
|
||||||
|
transport := util.GetHTTPTransport(registry.Insecure)
|
||||||
|
modifiers := []modifier.Modifier{
|
||||||
|
&auth.UserAgentModifier{
|
||||||
|
UserAgent: UserAgentReplication,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if authorizer != nil {
|
||||||
modifiers = append(modifiers, authorizer)
|
modifiers = append(modifiers, authorizer)
|
||||||
}
|
}
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
|
@ -63,11 +63,10 @@ func (n native) FetchImages(filters []*model.Filter) ([]*model.Resource, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n native) filterRepositories(pattern string) ([]string, error) {
|
func (n native) filterRepositories(pattern string) ([]string, error) {
|
||||||
// if is a specific repository name
|
// if the pattern is a specific repository name, just returns the parsed repositories
|
||||||
// just to make sure the repository exists
|
// and will check the existence later when filtering the tags
|
||||||
if len(pattern) > 0 && util.IsSpecificRepositoryName(pattern) {
|
if repositories, ok := util.IsSpecificPath(pattern); ok {
|
||||||
// check is repository exist later at filterTags.
|
return repositories, nil
|
||||||
return []string{pattern}, nil
|
|
||||||
}
|
}
|
||||||
// search repositories from catalog api
|
// search repositories from catalog api
|
||||||
repositories, err := n.Catalog()
|
repositories, err := n.Catalog()
|
||||||
|
121
src/replication/util/pattern.go
Normal file
121
src/replication/util/pattern.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bmatcuk/doublestar"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match returns whether the str matches the pattern
|
||||||
|
func Match(pattern, str string) (bool, error) {
|
||||||
|
if len(pattern) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return doublestar.Match(pattern, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSpecificPath checks whether the input path is a specified string
|
||||||
|
// If it is, the function returns a string array that parsed from the input path
|
||||||
|
// A specified string means we can get a specific string array after parsing it
|
||||||
|
// "library/hello-world" is a specified string as it only matches "library/hello-world"
|
||||||
|
// "library/**" isn't a specified string as it can match all string that starts with "library/"
|
||||||
|
// "library/{test,busybox}" is a specified string as it only matches "library/hello-world" and "library/busybox"
|
||||||
|
func IsSpecificPath(path string) ([]string, bool) {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
components := [][]string{}
|
||||||
|
for _, component := range strings.Split(path, "/") {
|
||||||
|
strs, ok := IsSpecificPathComponent(component)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
components = append(components, strs)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []string{}
|
||||||
|
for _, component := range components {
|
||||||
|
result = combinationPathComponents(result, component)
|
||||||
|
}
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func combinationPathComponents(components1, components2 []string) []string {
|
||||||
|
if len(components1) == 0 {
|
||||||
|
return components2
|
||||||
|
}
|
||||||
|
if len(components2) == 0 {
|
||||||
|
return components1
|
||||||
|
}
|
||||||
|
components := []string{}
|
||||||
|
for _, component1 := range components1 {
|
||||||
|
for _, component2 := range components2 {
|
||||||
|
components = append(components, fmt.Sprintf("%s/%s", component1, component2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return components
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSpecificPathComponent checks whether the input path component is a specified string
|
||||||
|
// If it is, the function returns a string array that parsed from the input component
|
||||||
|
// A specified string means we can get a specific string array after parsing it
|
||||||
|
// "library" is a specified string as it only matches "library"
|
||||||
|
// "library*" isn't a specified string as it can match all string that starts with "library"
|
||||||
|
// "{library, test}" is a specified string as it only matches "library" and "test"
|
||||||
|
// Note: the function doesn't support the component that contains more than one "{"
|
||||||
|
// such as "a{b{c,d}e}f"
|
||||||
|
func IsSpecificPathComponent(component string) ([]string, bool) {
|
||||||
|
if len(component) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
// contains any of *?[\\]^
|
||||||
|
if strings.ContainsAny(component, "*?[\\]^/") {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
// doesn't contain {},
|
||||||
|
if !strings.ContainsAny(component, "{},") {
|
||||||
|
return []string{component}, true
|
||||||
|
}
|
||||||
|
// support only one pair of {} currently
|
||||||
|
n := strings.Count(component, "{")
|
||||||
|
if n > 1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
i := strings.Index(component, "{")
|
||||||
|
if i == -1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
j := strings.LastIndex(component, "}")
|
||||||
|
if j == -1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if i > j {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
prefix := component[:i]
|
||||||
|
suffix := ""
|
||||||
|
if j+1 < len(component) {
|
||||||
|
suffix = component[j+1:]
|
||||||
|
}
|
||||||
|
components := []string{}
|
||||||
|
strs := strings.Split(component[i+1:j], ",")
|
||||||
|
for _, str := range strs {
|
||||||
|
components = append(components, prefix+str+suffix)
|
||||||
|
}
|
||||||
|
return components, true
|
||||||
|
}
|
178
src/replication/util/pattern_test.go
Normal file
178
src/replication/util/pattern_test.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
// 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatch(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
pattern string
|
||||||
|
str string
|
||||||
|
match bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pattern: "",
|
||||||
|
str: "library",
|
||||||
|
match: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "*",
|
||||||
|
str: "library",
|
||||||
|
match: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "*",
|
||||||
|
str: "library/hello-world",
|
||||||
|
match: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "**",
|
||||||
|
str: "library/hello-world",
|
||||||
|
match: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "{library,harbor}/**",
|
||||||
|
str: "library/hello-world",
|
||||||
|
match: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "{library,harbor}/**",
|
||||||
|
str: "harbor/hello-world",
|
||||||
|
match: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "1.?",
|
||||||
|
str: "1.0",
|
||||||
|
match: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "1.?",
|
||||||
|
str: "1.01",
|
||||||
|
match: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
match, err := Match(c.pattern, c.str)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, c.match, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsSpecificPathComponent(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
component string
|
||||||
|
isSpecific bool
|
||||||
|
resultComponents []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
component: "",
|
||||||
|
isSpecific: false,
|
||||||
|
resultComponents: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: "library/hello-world",
|
||||||
|
isSpecific: false,
|
||||||
|
resultComponents: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: "library",
|
||||||
|
isSpecific: true,
|
||||||
|
resultComponents: []string{"library"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: "lib*",
|
||||||
|
isSpecific: false,
|
||||||
|
resultComponents: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: "{library}",
|
||||||
|
isSpecific: true,
|
||||||
|
resultComponents: []string{"library"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: "{library,test}",
|
||||||
|
isSpecific: true,
|
||||||
|
resultComponents: []string{"library", "test"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: "{library{a}c}",
|
||||||
|
isSpecific: false,
|
||||||
|
resultComponents: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
fmt.Printf("running case %d ...\n", i)
|
||||||
|
components, ok := IsSpecificPathComponent(c.component)
|
||||||
|
require.Equal(t, c.isSpecific, ok)
|
||||||
|
require.Equal(t, len(c.resultComponents), len(components))
|
||||||
|
for i := range components {
|
||||||
|
assert.Equal(t, c.resultComponents[i], components[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsSpecificPath(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
path string
|
||||||
|
isSpecific bool
|
||||||
|
resultPaths []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
isSpecific: false,
|
||||||
|
resultPaths: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "library",
|
||||||
|
isSpecific: true,
|
||||||
|
resultPaths: []string{"library"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "library/hello-world",
|
||||||
|
isSpecific: true,
|
||||||
|
resultPaths: []string{"library/hello-world"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "library/**",
|
||||||
|
isSpecific: false,
|
||||||
|
resultPaths: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "{library}",
|
||||||
|
isSpecific: true,
|
||||||
|
resultPaths: []string{"library"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "library/{hello-world,busybox}",
|
||||||
|
isSpecific: true,
|
||||||
|
resultPaths: []string{"library/hello-world", "library/busybox"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, c := range cases {
|
||||||
|
fmt.Printf("running case %d ...\n", i)
|
||||||
|
paths, ok := IsSpecificPath(c.path)
|
||||||
|
require.Equal(t, c.isSpecific, ok)
|
||||||
|
require.Equal(t, len(c.resultPaths), len(paths))
|
||||||
|
for i := range paths {
|
||||||
|
assert.Equal(t, c.resultPaths[i], paths[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,31 +18,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
|
|
||||||
"github.com/bmatcuk/doublestar"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Match returns whether the str matches the pattern
|
|
||||||
func Match(pattern, str string) (bool, error) {
|
|
||||||
if len(pattern) == 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
match, err := doublestar.Match(pattern, str)
|
|
||||||
if err == doublestar.ErrBadPattern {
|
|
||||||
log.Warningf("failed to match the string %s against pattern %s: %v", str, pattern, err)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return match, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSpecificRepositoryName if the name not contains any char of "*?[{\\]}^,",
|
|
||||||
// it is a specific repository name.
|
|
||||||
func IsSpecificRepositoryName(name string) bool {
|
|
||||||
return !strings.ContainsAny(name, "*?[{\\]}^,")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHTTPTransport can be used to share the common HTTP transport
|
// GetHTTPTransport can be used to share the common HTTP transport
|
||||||
func GetHTTPTransport(insecure bool) *http.Transport {
|
func GetHTTPTransport(insecure bool) *http.Transport {
|
||||||
return registry.GetHTTPTransport(insecure)
|
return registry.GetHTTPTransport(insecure)
|
||||||
|
@ -18,68 +18,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMatch(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
pattern string
|
|
||||||
str string
|
|
||||||
match bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
pattern: "",
|
|
||||||
str: "library",
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "*",
|
|
||||||
str: "library",
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "*",
|
|
||||||
str: "library/hello-world",
|
|
||||||
match: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "**",
|
|
||||||
str: "library/hello-world",
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "{library,harbor}/**",
|
|
||||||
str: "library/hello-world",
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "{library,harbor}/**",
|
|
||||||
str: "harbor/hello-world",
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "1.?",
|
|
||||||
str: "1.0",
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "1.?",
|
|
||||||
str: "1.01",
|
|
||||||
match: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "a[",
|
|
||||||
str: "aaa",
|
|
||||||
match: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, c := range cases {
|
|
||||||
match, err := Match(c.pattern, c.str)
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Equal(t, c.match, match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetHTTPTransport(t *testing.T) {
|
func TestGetHTTPTransport(t *testing.T) {
|
||||||
transport := GetHTTPTransport(true)
|
transport := GetHTTPTransport(true)
|
||||||
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
|
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
|
||||||
@ -109,70 +49,3 @@ func TestParseRepository(t *testing.T) {
|
|||||||
assert.Equal(t, "a/b", namespace)
|
assert.Equal(t, "a/b", namespace)
|
||||||
assert.Equal(t, "c", rest)
|
assert.Equal(t, "c", rest)
|
||||||
}
|
}
|
||||||
func TestIsSpecificRepositoryName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"Is Specific", "a", true},
|
|
||||||
{"Is Specific", "abc", true},
|
|
||||||
{"Is Specific", "a/b", true},
|
|
||||||
{"Is Specific", "a/b/c", true},
|
|
||||||
{"Not Specific", "*", false},
|
|
||||||
{"Not Specific", "?", false},
|
|
||||||
{"Not Specific", "*c", false},
|
|
||||||
{"Not Specific", "a*", false},
|
|
||||||
{"Not Specific", "a*b*c*d*e*", false},
|
|
||||||
{"Not Specific", "a*b?c*x", false},
|
|
||||||
{"Not Specific", "ab[c]", false},
|
|
||||||
{"Not Specific", "ab[b-d]", false},
|
|
||||||
{"Not Specific", "ab[e-g]", false},
|
|
||||||
{"Not Specific", "ab[^c]", false},
|
|
||||||
{"Not Specific", "ab[^b-d]", false},
|
|
||||||
{"Not Specific", "ab[^e-g]", false},
|
|
||||||
{"Not Specific", "a\\*b", false},
|
|
||||||
{"Not Specific", "a?b", false},
|
|
||||||
{"Not Specific", "a[^a]b", false},
|
|
||||||
{"Not Specific", "a???b", false},
|
|
||||||
{"Not Specific", "a[^a][^a][^a]b", false},
|
|
||||||
{"Not Specific", "[a-ζ]*", false},
|
|
||||||
{"Not Specific", "*[a-ζ]", false},
|
|
||||||
{"Not Specific", "a?b", false},
|
|
||||||
{"Not Specific", "a*b", false},
|
|
||||||
{"Not Specific", "[\\-]", false},
|
|
||||||
{"Not Specific", "[x\\-]", false},
|
|
||||||
{"Not Specific", "[x\\-]", false},
|
|
||||||
{"Not Specific", "[x\\-]", false},
|
|
||||||
{"Not Specific", "[\\-x]", false},
|
|
||||||
{"Not Specific", "[\\-x]", false},
|
|
||||||
{"Not Specific", "[\\-x]", false},
|
|
||||||
{"Not Specific", "[a-b-c]", false},
|
|
||||||
{"Not Specific", "*x", false},
|
|
||||||
{"Not Specific", "[abc]", false},
|
|
||||||
{"Not Specific", "**", false},
|
|
||||||
{"Not Specific", "ab{c,d}", false},
|
|
||||||
{"Not Specific", "ab{c,d,*}", false},
|
|
||||||
{"Not Specific", "abc**", false},
|
|
||||||
{"Not Specific", "[]a]", false},
|
|
||||||
{"Not Specific", "[-]", false},
|
|
||||||
{"Not Specific", "[x-]", false},
|
|
||||||
{"Not Specific", "[-x]", false},
|
|
||||||
{"Not Specific", "\\", false},
|
|
||||||
{"Not Specific", "[a-b-c]", false},
|
|
||||||
{"Not Specific", "[]", false},
|
|
||||||
{"Not Specific", "[", false},
|
|
||||||
{"Not Specific", "[^", false},
|
|
||||||
{"Not Specific", "^", false},
|
|
||||||
{"Not Specific", "]", false},
|
|
||||||
{"Not Specific", "[^bc", false},
|
|
||||||
{"Not Specific", "a[", false},
|
|
||||||
{"Not Specific", "ab{c,d}[", false},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run("", func(t *testing.T) {
|
|
||||||
var got = IsSpecificRepositoryName(tt.input)
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user