Update the adapter interface

Add ConvertResourceMetadata and PrepareForPush methods

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2019-04-10 19:37:33 +08:00
parent ba20da5dd4
commit 5a047a7eb6
25 changed files with 441 additions and 219 deletions

View File

@ -333,8 +333,13 @@ func (ra *RepositoryAPI) Delete() {
Resource: &model.Resource{ Resource: &model.Resource{
Type: model.ResourceTypeRepository, Type: model.ResourceTypeRepository,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: repoName, Namespace: &model.Namespace{
Namespace: projectName, Name: projectName,
// TODO filling the metadata
},
Repository: &model.Repository{
Name: strings.TrimPrefix(repoName, projectName+"/"),
},
Vtags: []string{tag}, Vtags: []string{tag},
}, },
Deleted: true, Deleted: true,

View File

@ -120,8 +120,13 @@ func (n *NotificationHandler) Post() {
Resource: &model.Resource{ Resource: &model.Resource{
Type: model.ResourceTypeRepository, Type: model.ResourceTypeRepository,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: repository, Namespace: &model.Namespace{
Namespace: project, Name: project,
// TODO filling the metadata
},
Repository: &model.Repository{
Name: strings.TrimPrefix(repository, project+"/"),
},
Vtags: []string{tag}, Vtags: []string{tag},
}, },
}, },

View File

@ -33,12 +33,15 @@ type Adapter interface {
// Lists the available namespaces under the specified registry with the // Lists the available namespaces under the specified registry with the
// provided credential/token // provided credential/token
ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error)
// Create a new namespace // ConvertResourceMetadata converts the namespace and repository part of the resource metadata
// This method should guarantee it's idempotent // to the one that the adapter can handle
// And returns nil if a namespace with the same name already exists ConvertResourceMetadata(*model.ResourceMetadata, *model.Namespace) (*model.ResourceMetadata, error)
CreateNamespace(*model.Namespace) error // PrepareForPush does the prepare work that needed for pushing/uploading the resource
// eg: create the namespace or repository
PrepareForPush(*model.Resource) error
// Get the namespace specified by the name, the returning value should // Get the namespace specified by the name, the returning value should
// contain the metadata about the namespace if it has // contain the metadata about the namespace if it has
// TODO remove this method?
GetNamespace(string) (*model.Namespace, error) GetNamespace(string) (*model.Namespace, error)
// HealthCheck checks health status of registry // HealthCheck checks health status of registry
HealthCheck() (model.HealthStatus, error) HealthCheck() (model.HealthStatus, error)

View File

@ -15,8 +15,10 @@
package harbor package harbor
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
common_http "github.com/goharbor/harbor/src/common/http" common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier" "github.com/goharbor/harbor/src/common/http/modifier"
@ -86,6 +88,7 @@ func newAdapter(registry *model.Registry) *adapter {
func (a *adapter) Info() (*model.RegistryInfo, error) { func (a *adapter) Info() (*model.RegistryInfo, error) {
info := &model.RegistryInfo{ info := &model.RegistryInfo{
Type: model.RegistryTypeHarbor, Type: model.RegistryTypeHarbor,
SupportNamespace: true,
SupportedResourceTypes: []model.ResourceType{ SupportedResourceTypes: []model.ResourceType{
model.ResourceTypeRepository, model.ResourceTypeRepository,
}, },
@ -126,13 +129,63 @@ func (a *adapter) Info() (*model.RegistryInfo, error) {
func (a *adapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) { func (a *adapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) {
return nil, nil return nil, nil
} }
func (a *adapter) CreateNamespace(namespace *model.Namespace) error { func (a *adapter) ConvertResourceMetadata(metadata *model.ResourceMetadata, namespace *model.Namespace) (*model.ResourceMetadata, error) {
if metadata == nil {
return nil, errors.New("the metadata cannot be null")
}
name := metadata.GetResourceName()
strs := strings.SplitN(name, "/", 2)
if len(strs) < 2 {
return nil, fmt.Errorf("unsupported resource name %s, at least contains one '/'", name)
}
meta := &model.ResourceMetadata{
Vtags: metadata.Vtags,
Labels: metadata.Labels,
}
meta.Namespace = &model.Namespace{
Name: strs[0],
}
if metadata.Namespace != nil {
meta.Namespace.Metadata = metadata.Namespace.Metadata
}
meta.Repository = &model.Repository{
Name: strs[1],
}
if metadata.Repository != nil {
meta.Repository.Metadata = metadata.Repository.Metadata
}
// replace the namespace if it is specified
if namespace == nil || len(namespace.Name) == 0 {
return meta, nil
}
if strings.Contains(namespace.Name, "/") {
return nil, fmt.Errorf("the namespace %s cannot contain '/'", namespace.Name)
}
meta.Namespace.Name = namespace.Name
if namespace.Metadata != nil {
meta.Namespace.Metadata = namespace.Metadata
}
return meta, nil
}
func (a *adapter) PrepareForPush(resource *model.Resource) error {
if resource == nil {
return errors.New("the resource cannot be null")
}
if resource.Metadata == nil {
return errors.New("the metadata of resource cannot be null")
}
if resource.Metadata.Namespace == nil {
return errors.New("the namespace of resource cannot be null")
}
if len(resource.Metadata.Namespace.Name) == 0 {
return errors.New("the name of the namespace cannot be null")
}
project := &struct { project := &struct {
Name string `json:"project_name"` Name string `json:"project_name"`
Metadata map[string]interface{} `json:"metadata"` Metadata map[string]interface{} `json:"metadata"`
}{ }{
Name: namespace.Name, Name: resource.Metadata.Namespace.Name,
Metadata: namespace.Metadata, Metadata: resource.Metadata.Namespace.Metadata,
} }
// TODO // TODO
@ -160,11 +213,13 @@ func (a *adapter) CreateNamespace(namespace *model.Namespace) error {
err := a.client.Post(a.coreServiceURL+"/api/projects", project) err := a.client.Post(a.coreServiceURL+"/api/projects", project)
if httpErr, ok := err.(*common_http.Error); ok && httpErr.Code == http.StatusConflict { if httpErr, ok := err.(*common_http.Error); ok && httpErr.Code == http.StatusConflict {
log.Debugf("got 409 when trying to create project %s", namespace.Name) log.Debugf("got 409 when trying to create project %s", resource.Metadata.Namespace.Name)
return nil return nil
} }
return err return err
} }
// TODO remove this method
func (a *adapter) GetNamespace(namespace string) (*model.Namespace, error) { func (a *adapter) GetNamespace(namespace string) (*model.Namespace, error) {
project, err := a.getProject(namespace) project, err := a.getProject(namespace)
if err != nil { if err != nil {

View File

@ -18,12 +18,10 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/replication/ng/model" "github.com/goharbor/harbor/src/replication/ng/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestInfo(t *testing.T) { func TestInfo(t *testing.T) {
@ -77,8 +75,7 @@ func TestListNamespaces(t *testing.T) {
// TODO // TODO
} }
func TestCreateNamespace(t *testing.T) { func TestPrepareForPush(t *testing.T) {
// project doesn't exist
server := test.NewServer(&test.RequestHandlerMapping{ server := test.NewServer(&test.RequestHandlerMapping{
Method: http.MethodPost, Method: http.MethodPost,
Pattern: "/api/projects", Pattern: "/api/projects",
@ -90,10 +87,41 @@ func TestCreateNamespace(t *testing.T) {
URL: server.URL, URL: server.URL,
} }
adapter := newAdapter(registry) adapter := newAdapter(registry)
err := adapter.CreateNamespace(&model.Namespace{ // nil resource
err := adapter.PrepareForPush(nil)
require.NotNil(t, err)
// nil metadata
err = adapter.PrepareForPush(&model.Resource{})
require.NotNil(t, err)
// nil namespace
err = adapter.PrepareForPush(&model.Resource{
Metadata: &model.ResourceMetadata{},
})
require.NotNil(t, err)
// nil namespace name
err = adapter.PrepareForPush(&model.Resource{
Metadata: &model.ResourceMetadata{
Namespace: &model.Namespace{},
},
})
require.NotNil(t, err)
// nil namespace name
err = adapter.PrepareForPush(&model.Resource{
Metadata: &model.ResourceMetadata{
Namespace: &model.Namespace{},
},
})
require.NotNil(t, err)
// project doesn't exist
err = adapter.PrepareForPush(&model.Resource{
Metadata: &model.ResourceMetadata{
Namespace: &model.Namespace{
Name: "library", Name: "library",
},
},
}) })
require.Nil(t, err) require.Nil(t, err)
server.Close() server.Close()
// project already exists // project already exists
@ -108,11 +136,14 @@ func TestCreateNamespace(t *testing.T) {
URL: server.URL, URL: server.URL,
} }
adapter = newAdapter(registry) adapter = newAdapter(registry)
err = adapter.CreateNamespace(&model.Namespace{ err = adapter.PrepareForPush(&model.Resource{
Metadata: &model.ResourceMetadata{
Namespace: &model.Namespace{
Name: "library", Name: "library",
},
},
}) })
require.Nil(t, err) require.Nil(t, err)
server.Close()
} }
func TestGetNamespace(t *testing.T) { func TestGetNamespace(t *testing.T) {

View File

@ -67,8 +67,13 @@ func (a *adapter) FetchCharts(namespaces []string, filters []*model.Filter) ([]*
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Registry: a.registry, Registry: a.registry,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Namespace: namespace, Namespace: &model.Namespace{
Name: fmt.Sprintf("%s/%s", namespace, chart.Name), Name: namespace,
// TODO filling the metadata
},
Repository: &model.Repository{
Name: chart.Name,
},
Vtags: []string{version.Version}, Vtags: []string{version.Version},
}, },
}) })

View File

@ -61,8 +61,8 @@ func TestFetchCharts(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, 2, len(resources)) assert.Equal(t, 2, len(resources))
assert.Equal(t, model.ResourceTypeChart, resources[0].Type) assert.Equal(t, model.ResourceTypeChart, resources[0].Type)
assert.Equal(t, "library/harbor", resources[0].Metadata.Name) assert.Equal(t, "harbor", resources[0].Metadata.Repository.Name)
assert.Equal(t, "library", resources[0].Metadata.Namespace) assert.Equal(t, "library", resources[0].Metadata.Namespace.Name)
assert.Equal(t, 1, len(resources[0].Metadata.Vtags)) assert.Equal(t, 1, len(resources[0].Metadata.Vtags))
assert.Equal(t, "1.0", resources[0].Metadata.Vtags[0]) assert.Equal(t, "1.0", resources[0].Metadata.Vtags[0])
} }

View File

@ -16,6 +16,7 @@ package harbor
import ( import (
"fmt" "fmt"
"strings"
"github.com/goharbor/harbor/src/replication/ng/model" "github.com/goharbor/harbor/src/replication/ng/model"
) )
@ -56,8 +57,13 @@ func (a *adapter) FetchImages(namespaces []string, filters []*model.Filter) ([]*
Type: model.ResourceTypeRepository, Type: model.ResourceTypeRepository,
Registry: a.registry, Registry: a.registry,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Namespace: namespace, Namespace: &model.Namespace{
Name: repository.Name, Name: namespace,
// TODO filling the metadata
},
Repository: &model.Repository{
Name: strings.TrimPrefix(repository.Name, namespace+"/"),
},
Vtags: vtags, Vtags: vtags,
}, },
}) })

View File

@ -71,8 +71,8 @@ func TestFetchImages(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, 1, len(resources)) assert.Equal(t, 1, len(resources))
assert.Equal(t, model.ResourceTypeRepository, resources[0].Type) assert.Equal(t, model.ResourceTypeRepository, resources[0].Type)
assert.Equal(t, "library/hello-world", resources[0].Metadata.Name) assert.Equal(t, "hello-world", resources[0].Metadata.Repository.Name)
assert.Equal(t, "library", resources[0].Metadata.Namespace) assert.Equal(t, "library", resources[0].Metadata.Namespace.Name)
assert.Equal(t, 2, len(resources[0].Metadata.Vtags)) assert.Equal(t, 2, len(resources[0].Metadata.Vtags))
assert.Equal(t, "1.0", resources[0].Metadata.Vtags[0]) assert.Equal(t, "1.0", resources[0].Metadata.Vtags[0])
assert.Equal(t, "2.0", resources[0].Metadata.Vtags[1]) assert.Equal(t, "2.0", resources[0].Metadata.Vtags[1])

View File

@ -57,9 +57,9 @@ func (h *handler) Handle(event *Event) error {
var err error var err error
switch event.Type { switch event.Type {
case EventTypeImagePush, EventTypeChartUpload: case EventTypeImagePush, EventTypeChartUpload:
policies, err = h.getRelatedPolicies(event.Resource.Metadata.Namespace) policies, err = h.getRelatedPolicies(event.Resource.Metadata.Namespace.Name)
case EventTypeImageDelete, EventTypeChartDelete: case EventTypeImageDelete, EventTypeChartDelete:
policies, err = h.getRelatedPolicies(event.Resource.Metadata.Namespace, true) policies, err = h.getRelatedPolicies(event.Resource.Metadata.Namespace.Name, true)
default: default:
return fmt.Errorf("unsupported event type %s", event.Type) return fmt.Errorf("unsupported event type %s", event.Type)
} }

View File

@ -161,8 +161,12 @@ func TestHandle(t *testing.T) {
err = handler.Handle(&Event{ err = handler.Handle(&Event{
Resource: &model.Resource{ Resource: &model.Resource{
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{}, Vtags: []string{},
}, },
}, },
@ -174,8 +178,12 @@ func TestHandle(t *testing.T) {
err = handler.Handle(&Event{ err = handler.Handle(&Event{
Resource: &model.Resource{ Resource: &model.Resource{
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
}, },
@ -187,8 +195,12 @@ func TestHandle(t *testing.T) {
err = handler.Handle(&Event{ err = handler.Handle(&Event{
Resource: &model.Resource{ Resource: &model.Resource{
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
}, },
@ -200,8 +212,12 @@ func TestHandle(t *testing.T) {
err = handler.Handle(&Event{ err = handler.Handle(&Event{
Resource: &model.Resource{ Resource: &model.Resource{
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
}, },

View File

@ -108,6 +108,7 @@ type FilterStyle struct {
type RegistryInfo struct { type RegistryInfo struct {
Type RegistryType `json:"type"` Type RegistryType `json:"type"`
Description string `json:"description"` Description string `json:"description"`
SupportNamespace bool `json:"support_namespace"`
SupportedResourceTypes []ResourceType `json:"-"` SupportedResourceTypes []ResourceType `json:"-"`
SupportedResourceFilters []*FilterStyle `json:"supported_resource_filters"` SupportedResourceFilters []*FilterStyle `json:"supported_resource_filters"`
SupportedTriggers []TriggerType `json:"supported_triggers"` SupportedTriggers []TriggerType `json:"supported_triggers"`

View File

@ -30,12 +30,34 @@ func (r ResourceType) Valid() bool {
// ResourceMetadata of resource // ResourceMetadata of resource
type ResourceMetadata struct { type ResourceMetadata struct {
Namespace string `json:"namespace"` Namespace *Namespace `json:"namespace"`
Name string `json:"name"` Repository *Repository `json:"repository"`
Vtags []string `json:"v_tags"` Vtags []string `json:"v_tags"`
// TODO the labels should be put into tag and repository level?
Labels []string `json:"labels"` Labels []string `json:"labels"`
} }
// GetResourceName returns the name of the resource
func (r *ResourceMetadata) GetResourceName() string {
name := ""
if r.Namespace != nil && len(r.Namespace.Name) > 0 {
name += r.Namespace.Name
}
if r.Repository != nil && len(r.Repository.Name) > 0 {
if len(name) > 0 {
name += "/"
}
name += r.Repository.Name
}
return name
}
// Repository info of the resource
type Repository struct {
Name string `json:"name"`
Metadata map[string]interface{} `json:"metadata"`
}
// Resource represents the general replicating content // Resource represents the general replicating content
type Resource struct { type Resource struct {
Type ResourceType `json:"type"` Type ResourceType `json:"type"`

View File

@ -0,0 +1,50 @@
// 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 model
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetResourceName(t *testing.T) {
r := &ResourceMetadata{}
assert.Equal(t, "", r.GetResourceName())
r = &ResourceMetadata{
Namespace: &Namespace{
Name: "library",
},
}
assert.Equal(t, "library", r.GetResourceName())
r = &ResourceMetadata{
Repository: &Repository{
Name: "hello-world",
},
}
assert.Equal(t, "hello-world", r.GetResourceName())
r = &ResourceMetadata{
Namespace: &Namespace{
Name: "library",
},
Repository: &Repository{
Name: "hello-world",
},
}
assert.Equal(t, "library/hello-world", r.GetResourceName())
}

View File

@ -105,7 +105,8 @@ func (c *controller) createFlow(executionID int64, policy *model.Policy, resourc
}, },
{ {
Type: model.FilterTypeName, Type: model.FilterTypeName,
Value: resource.Metadata.Name, // TODO only filter the repo part?
Value: resource.Metadata.GetResourceName(),
}, },
{ {
Type: model.FilterTypeTag, Type: model.FilterTypeTag,

View File

@ -132,7 +132,18 @@ func (f *fakedAdapter) Info() (*model.RegistryInfo, error) {
func (f *fakedAdapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) { func (f *fakedAdapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) {
return nil, nil return nil, nil
} }
func (f *fakedAdapter) CreateNamespace(*model.Namespace) error { func (f *fakedAdapter) ConvertResourceMetadata(*model.ResourceMetadata, *model.Namespace) (*model.ResourceMetadata, error) {
return &model.ResourceMetadata{
Namespace: &model.Namespace{
Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"},
}, nil
}
func (f *fakedAdapter) PrepareForPush(*model.Resource) error {
return nil return nil
} }
func (f *fakedAdapter) HealthCheck() (model.HealthStatus, error) { func (f *fakedAdapter) HealthCheck() (model.HealthStatus, error) {
@ -155,8 +166,12 @@ func (f *fakedAdapter) FetchImages(namespace []string, filters []*model.Filter)
{ {
Type: model.ResourceTypeRepository, Type: model.ResourceTypeRepository,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
Override: false, Override: false,
@ -190,8 +205,12 @@ func (f *fakedAdapter) FetchCharts(namespaces []string, filters []*model.Filter)
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/harbor", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "harbor",
},
Vtags: []string{"0.2.0"}, Vtags: []string{"0.2.0"},
}, },
}, },
@ -232,7 +251,12 @@ func TestStartReplication(t *testing.T) {
resource := &model.Resource{ resource := &model.Resource{
Type: model.ResourceTypeRepository, Type: model.ResourceTypeRepository,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"1.0", "2.0"}, Vtags: []string{"1.0", "2.0"},
}, },
} }

View File

@ -57,14 +57,13 @@ func (c *copyFlow) Run(interface{}) (int, error) {
log.Infof("no resources need to be replicated for the execution %d, skip", c.executionID) log.Infof("no resources need to be replicated for the execution %d, skip", c.executionID)
return 0, nil return 0, nil
} }
dstNamespaces, err := assembleDestinationNamespaces(srcAdapter, srcResources, c.policy.DestNamespace) dstResources, err := assembleDestinationResources(dstAdapter, srcResources, c.policy)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if err = createNamespaces(dstAdapter, dstNamespaces); err != nil { if err = prepareForPush(dstAdapter, dstResources); err != nil {
return 0, err return 0, err
} }
dstResources := assembleDestinationResources(srcResources, c.policy.DestRegistry, c.policy.DestNamespace, c.policy.Override)
items, err := preprocess(c.scheduler, srcResources, dstResources) items, err := preprocess(c.scheduler, srcResources, dstResources)
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -43,6 +43,10 @@ func NewDeletionFlow(executionMgr execution.Manager, scheduler scheduler.Schedul
} }
func (d *deletionFlow) Run(interface{}) (int, error) { func (d *deletionFlow) Run(interface{}) (int, error) {
_, dstAdapter, err := initialize(d.policy)
if err != nil {
return 0, err
}
// filling the registry information // filling the registry information
for _, resource := range d.resources { for _, resource := range d.resources {
resource.Registry = d.policy.SrcRegistry resource.Registry = d.policy.SrcRegistry
@ -56,7 +60,10 @@ func (d *deletionFlow) Run(interface{}) (int, error) {
log.Infof("no resources need to be replicated for the execution %d, skip", d.executionID) log.Infof("no resources need to be replicated for the execution %d, skip", d.executionID)
return 0, nil return 0, nil
} }
dstResources := assembleDestinationResources(srcResources, d.policy.DestRegistry, d.policy.DestNamespace, d.policy.Override) dstResources, err := assembleDestinationResources(dstAdapter, srcResources, d.policy)
if err != nil {
return 0, err
}
items, err := preprocess(d.scheduler, srcResources, dstResources) items, err := preprocess(d.scheduler, srcResources, dstResources)
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -37,8 +37,12 @@ func TestRunOfDeletionFlow(t *testing.T) {
resources := []*model.Resource{ resources := []*model.Resource{
{ {
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
}, },

View File

@ -134,7 +134,8 @@ func filterResources(resources []*model.Resource, filters []*model.Filter) ([]*m
match = false match = false
break break
} }
m, err := util.Match(pattern, resource.Metadata.Name) // TODO filter only the repository part?
m, err := util.Match(pattern, resource.Metadata.GetResourceName())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -181,73 +182,46 @@ func filterResources(resources []*model.Resource, filters []*model.Filter) ([]*m
return res, nil return res, nil
} }
// Assemble the namespaces that need to be created on the destination registry: // assemble the destination resources by filling the metadata, registry and override properties
// step 1: get the detail information for each of the source namespaces func assembleDestinationResources(adapter adp.Adapter, resources []*model.Resource,
// step 2: if the destination namespace isn't specified in the policy, then the policy *model.Policy) ([]*model.Resource, error) {
// same namespaces with the source will be returned. If it is specified, then
// returns the specified one with the merged metadatas of all source namespaces
func assembleDestinationNamespaces(srcAdapter adp.Adapter, srcResources []*model.Resource, dstNamespace string) ([]*model.Namespace, error) {
namespaces := []*model.Namespace{}
for _, srcResource := range srcResources {
namespace, err := srcAdapter.GetNamespace(srcResource.Metadata.Namespace)
if err != nil {
return nil, err
}
namespaces = append(namespaces, namespace)
}
if len(dstNamespace) != 0 {
namespaces = []*model.Namespace{
{
Name: dstNamespace,
// TODO merge the metadata
Metadata: map[string]interface{}{},
},
}
}
log.Debug("assemble the destination namespaces completed")
return namespaces, nil
}
// create the namespaces on the destination registry
func createNamespaces(adapter adp.Adapter, namespaces []*model.Namespace) error {
for _, namespace := range namespaces {
if err := adapter.CreateNamespace(namespace); err != nil {
return fmt.Errorf("failed to create the namespace %s on the destination registry: %v", namespace.Name, err)
}
log.Debugf("namespace %s created on the destination registry", namespace.Name)
}
return nil
}
// assemble the destination resources by filling the registry, namespace and override properties
func assembleDestinationResources(resources []*model.Resource,
registry *model.Registry, namespace string, override bool) []*model.Resource {
result := []*model.Resource{} result := []*model.Resource{}
var namespace *model.Namespace
if len(policy.DestNamespace) > 0 {
namespace = &model.Namespace{
Name: policy.DestNamespace,
}
}
for _, resource := range resources { for _, resource := range resources {
metadata, err := adapter.ConvertResourceMetadata(resource.Metadata, namespace)
if err != nil {
return nil, fmt.Errorf("failed to convert the resource metadata of %s: %v", resource.Metadata.GetResourceName(), err)
}
res := &model.Resource{ res := &model.Resource{
Type: resource.Type, Type: resource.Type,
Metadata: &model.ResourceMetadata{ Metadata: metadata,
Name: resource.Metadata.Name, Registry: policy.DestRegistry,
Namespace: resource.Metadata.Namespace,
Vtags: resource.Metadata.Vtags,
},
Registry: registry,
ExtendedInfo: resource.ExtendedInfo, ExtendedInfo: resource.ExtendedInfo,
Deleted: resource.Deleted, Deleted: resource.Deleted,
Override: override, Override: policy.Override,
}
// if the destination namespace is specified, use the specified one
if len(namespace) > 0 {
res.Metadata.Name = strings.Replace(resource.Metadata.Name,
resource.Metadata.Namespace, namespace, 1)
res.Metadata.Namespace = namespace
} }
result = append(result, res) result = append(result, res)
} }
log.Debug("assemble the destination resources completed") log.Debug("assemble the destination resources completed")
return result return result, nil
}
// do the prepare work for pushing/uploading the resources: create the namespace or repository
func prepareForPush(adapter adp.Adapter, resources []*model.Resource) error {
// TODO need to consider how to handle that both contains public/private namespace
for _, resource := range resources {
name := resource.Metadata.GetResourceName()
if err := adapter.PrepareForPush(resource); err != nil {
return fmt.Errorf("failed to do the prepare work for pushing/uploading %s: %v", name, err)
}
log.Debugf("the prepare work for pushing/uploading %s completed", name)
}
return nil
} }
// preprocess // preprocess
@ -341,7 +315,7 @@ func getResourceName(res *model.Resource) string {
return "" return ""
} }
if len(meta.Vtags) == 0 { if len(meta.Vtags) == 0 {
return meta.Name return meta.GetResourceName()
} }
return meta.Name + ":[" + strings.Join(meta.Vtags, ",") + "]" return meta.GetResourceName() + ":[" + strings.Join(meta.Vtags, ",") + "]"
} }

View File

@ -49,7 +49,14 @@ func (f *fakedAdapter) Info() (*model.RegistryInfo, error) {
func (f *fakedAdapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) { func (f *fakedAdapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace, error) {
return nil, nil return nil, nil
} }
func (f *fakedAdapter) CreateNamespace(*model.Namespace) error { func (f *fakedAdapter) ConvertResourceMetadata(metadata *model.ResourceMetadata, namespace *model.Namespace) (*model.ResourceMetadata, error) {
if namespace != nil {
metadata.Namespace = namespace
}
return metadata, nil
}
func (f *fakedAdapter) PrepareForPush(*model.Resource) error {
return nil return nil
} }
func (f *fakedAdapter) HealthCheck() (model.HealthStatus, error) { func (f *fakedAdapter) HealthCheck() (model.HealthStatus, error) {
@ -72,8 +79,12 @@ func (f *fakedAdapter) FetchImages(namespace []string, filters []*model.Filter)
{ {
Type: model.ResourceTypeRepository, Type: model.ResourceTypeRepository,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
Override: false, Override: false,
@ -107,8 +118,12 @@ func (f *fakedAdapter) FetchCharts(namespaces []string, filters []*model.Filter)
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/harbor", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "harbor",
},
Vtags: []string{"0.2.0"}, Vtags: []string{"0.2.0"},
}, },
}, },
@ -226,8 +241,12 @@ func TestFilterResources(t *testing.T) {
{ {
Type: model.ResourceTypeRepository, Type: model.ResourceTypeRepository,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
// TODO test labels // TODO test labels
Labels: nil, Labels: nil,
@ -238,8 +257,12 @@ func TestFilterResources(t *testing.T) {
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/harbor", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "harbor",
},
Vtags: []string{"0.2.0", "0.3.0"}, Vtags: []string{"0.2.0", "0.3.0"},
// TODO test labels // TODO test labels
Labels: nil, Labels: nil,
@ -250,8 +273,12 @@ func TestFilterResources(t *testing.T) {
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/mysql", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "mysql",
},
Vtags: []string{"1.0"}, Vtags: []string{"1.0"},
// TODO test labels // TODO test labels
Labels: nil, Labels: nil,
@ -281,67 +308,40 @@ func TestFilterResources(t *testing.T) {
res, err := filterResources(resources, filters) res, err := filterResources(resources, filters)
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, 1, len(res)) assert.Equal(t, 1, len(res))
assert.Equal(t, "library/harbor", res[0].Metadata.Name) assert.Equal(t, "library", res[0].Metadata.Namespace.Name)
assert.Equal(t, "harbor", res[0].Metadata.Repository.Name)
assert.Equal(t, 1, len(res[0].Metadata.Vtags)) assert.Equal(t, 1, len(res[0].Metadata.Vtags))
assert.Equal(t, "0.2.0", res[0].Metadata.Vtags[0]) assert.Equal(t, "0.2.0", res[0].Metadata.Vtags[0])
} }
func TestAssembleDestinationNamespaces(t *testing.T) {
adapter := &fakedAdapter{}
resources := []*model.Resource{
{
Metadata: &model.ResourceMetadata{
Namespace: "library",
},
},
}
namespace := ""
ns, err := assembleDestinationNamespaces(adapter, resources, namespace)
require.Nil(t, err)
assert.Equal(t, 1, len(ns))
assert.Equal(t, "library", ns[0].Name)
assert.Equal(t, true, ns[0].Metadata["public"].(bool))
namespace = "test"
ns, err = assembleDestinationNamespaces(adapter, resources, namespace)
require.Nil(t, err)
assert.Equal(t, 1, len(ns))
assert.Equal(t, "test", ns[0].Name)
// TODO add test for merged metadata
// assert.Equal(t, true, ns[0].Metadata["public"].(bool))
}
func TestCreateNamespaces(t *testing.T) {
adapter := &fakedAdapter{}
namespaces := []*model.Namespace{
{
Name: "library",
},
}
err := createNamespaces(adapter, namespaces)
require.Nil(t, err)
}
func TestAssembleDestinationResources(t *testing.T) { func TestAssembleDestinationResources(t *testing.T) {
adapter := &fakedAdapter{}
resources := []*model.Resource{ resources := []*model.Resource{
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
Override: false, Override: false,
}, },
} }
registry := &model.Registry{} policy := &model.Policy{
namespace := "test" DestRegistry: &model.Registry{},
override := true DestNamespace: "test",
res := assembleDestinationResources(resources, registry, namespace, override) Override: true,
}
res, err := assembleDestinationResources(adapter, resources, policy)
require.Nil(t, err)
assert.Equal(t, 1, len(res)) assert.Equal(t, 1, len(res))
assert.Equal(t, model.ResourceTypeChart, res[0].Type) assert.Equal(t, model.ResourceTypeChart, res[0].Type)
assert.Equal(t, "test/hello-world", res[0].Metadata.Name) assert.Equal(t, "hello-world", res[0].Metadata.Repository.Name)
assert.Equal(t, namespace, res[0].Metadata.Namespace) assert.Equal(t, "test", res[0].Metadata.Namespace.Name)
assert.Equal(t, 1, len(res[0].Metadata.Vtags)) assert.Equal(t, 1, len(res[0].Metadata.Vtags))
assert.Equal(t, "latest", res[0].Metadata.Vtags[0]) assert.Equal(t, "latest", res[0].Metadata.Vtags[0])
} }
@ -352,8 +352,12 @@ func TestPreprocess(t *testing.T) {
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/hello-world", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
Override: false, Override: false,
@ -363,8 +367,12 @@ func TestPreprocess(t *testing.T) {
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "test/hello-world", Namespace: &model.Namespace{
Namespace: "test", Name: "test",
},
Repository: &model.Repository{
Name: "hello-world",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
}, },
Override: false, Override: false,

View File

@ -50,7 +50,9 @@ func TestStop(t *testing.T) {
func generateData() ([]*ScheduleItem, error) { func generateData() ([]*ScheduleItem, error) {
srcResource := &model.Resource{ srcResource := &model.Resource{
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Namespace: "namespace1", Namespace: &model.Namespace{
Name: "namespace1",
},
Vtags: []string{"latest"}, Vtags: []string{"latest"},
Labels: []string{"latest"}, Labels: []string{"latest"},
}, },
@ -60,7 +62,9 @@ func generateData() ([]*ScheduleItem, error) {
} }
destResource := &model.Resource{ destResource := &model.Resource{
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Namespace: "namespace2", Namespace: &model.Namespace{
Name: "namespace2",
},
Vtags: []string{"v1", "v2"}, Vtags: []string{"v1", "v2"},
Labels: []string{"latest"}, Labels: []string{"latest"},
}, },

View File

@ -17,10 +17,9 @@ package chart
import ( import (
"errors" "errors"
"github.com/goharbor/harbor/src/replication/ng/adapter"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/jobservice/errs" "github.com/goharbor/harbor/src/jobservice/errs"
"github.com/goharbor/harbor/src/replication/ng/adapter"
"github.com/goharbor/harbor/src/replication/ng/model" "github.com/goharbor/harbor/src/replication/ng/model"
trans "github.com/goharbor/harbor/src/replication/ng/transfer" trans "github.com/goharbor/harbor/src/replication/ng/transfer"
) )
@ -63,17 +62,17 @@ func (t *transfer) Transfer(src *model.Resource, dst *model.Resource) error {
// delete the chart on destination registry // delete the chart on destination registry
if dst.Deleted { if dst.Deleted {
return t.delete(&chart{ return t.delete(&chart{
name: dst.Metadata.Name, name: dst.Metadata.GetResourceName(),
version: dst.Metadata.Vtags[0], version: dst.Metadata.Vtags[0],
}) })
} }
srcChart := &chart{ srcChart := &chart{
name: src.Metadata.Name, name: src.Metadata.GetResourceName(),
version: src.Metadata.Vtags[0], version: src.Metadata.Vtags[0],
} }
dstChart := &chart{ dstChart := &chart{
name: dst.Metadata.Name, name: dst.Metadata.GetResourceName(),
version: dst.Metadata.Vtags[0], version: dst.Metadata.Vtags[0],
} }
// copy the chart from source registry to the destination // copy the chart from source registry to the destination

View File

@ -34,8 +34,12 @@ func (f *fakeRegistry) FetchCharts(namespaces []string, filters []*model.Filter)
{ {
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Name: "library/harbor", Namespace: &model.Namespace{
Namespace: "library", Name: "library",
},
Repository: &model.Repository{
Name: "harbor",
},
Vtags: []string{"0.2.0"}, Vtags: []string{"0.2.0"},
}, },
}, },

View File

@ -18,13 +18,12 @@ import (
"errors" "errors"
"strings" "strings"
"github.com/goharbor/harbor/src/replication/ng/adapter"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/jobservice/errs" "github.com/goharbor/harbor/src/jobservice/errs"
"github.com/goharbor/harbor/src/replication/ng/adapter"
"github.com/goharbor/harbor/src/replication/ng/model" "github.com/goharbor/harbor/src/replication/ng/model"
trans "github.com/goharbor/harbor/src/replication/ng/transfer" trans "github.com/goharbor/harbor/src/replication/ng/transfer"
) )
@ -67,17 +66,17 @@ func (t *transfer) Transfer(src *model.Resource, dst *model.Resource) error {
// delete the repository on destination registry // delete the repository on destination registry
if dst.Deleted { if dst.Deleted {
return t.delete(&repository{ return t.delete(&repository{
repository: dst.Metadata.Name, repository: dst.Metadata.GetResourceName(),
tags: dst.Metadata.Vtags, tags: dst.Metadata.Vtags,
}) })
} }
srcRepo := &repository{ srcRepo := &repository{
repository: src.Metadata.Name, repository: src.Metadata.GetResourceName(),
tags: src.Metadata.Vtags, tags: src.Metadata.Vtags,
} }
dstRepo := &repository{ dstRepo := &repository{
repository: dst.Metadata.Name, repository: dst.Metadata.GetResourceName(),
tags: dst.Metadata.Vtags, tags: dst.Metadata.Vtags,
} }
// copy the repository from source registry to the destination // copy the repository from source registry to the destination