Support replication between Harbor 2.0 and 1.x

Fixes #11374, fixes #11302, support replication between Harbor 2.0 and 1.x by providing versioning adapter

Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
Wenkai Yin 2020-04-04 20:59:46 +08:00
parent df490d0cea
commit 8f11cb7ff0
16 changed files with 819 additions and 484 deletions

View File

@ -0,0 +1,54 @@
// 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 (
"github.com/goharbor/harbor/src/lib/log"
adp "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/adapter/harbor/base"
v1 "github.com/goharbor/harbor/src/replication/adapter/harbor/v1"
v2 "github.com/goharbor/harbor/src/replication/adapter/harbor/v2"
"github.com/goharbor/harbor/src/replication/model"
)
func init() {
if err := adp.RegisterFactory(model.RegistryTypeHarbor, new(factory)); err != nil {
log.Errorf("failed to register factory for %s: %v", model.RegistryTypeHarbor, err)
return
}
log.Infof("the factory for adapter %s registered", model.RegistryTypeHarbor)
}
type factory struct {
}
func (f *factory) Create(r *model.Registry) (adp.Adapter, error) {
base, err := base.New(r)
if err != nil {
return nil, err
}
version := base.GetAPIVersion()
// no API version, it's instance of Harbor 1.x
if len(version) == 0 {
log.Debug("no API version, create the v1 adapter")
return v1.New(base), nil
}
log.Debugf("API version is %s, create the v2 adapter", version)
return v2.New(base), nil
}
func (f *factory) AdapterPattern() *model.AdapterPattern {
return nil
}

View File

@ -12,81 +12,48 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package harbor package base
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/goharbor/harbor/src/common/api"
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"
common_http_auth "github.com/goharbor/harbor/src/common/http/modifier/auth" common_http_auth "github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/jobservice/config"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/registry/auth/basic" "github.com/goharbor/harbor/src/pkg/registry/auth/basic"
adp "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/adapter/native" "github.com/goharbor/harbor/src/replication/adapter/native"
"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() { // New creates an instance of the base adapter
if err := adp.RegisterFactory(model.RegistryTypeHarbor, new(factory)); err != nil { func New(registry *model.Registry) (*Adapter, error) {
log.Errorf("failed to register factory for %s: %v", model.RegistryTypeHarbor, err) if isLocalHarbor(registry) {
return // when the adapter is created for local Harbor, returns the "http://127.0.0.1:8080"
// as URL to avoid issue https://github.com/goharbor/harbor-helm/issues/222
// when harbor is deployed on Kubernetes
url := "http://127.0.0.1:8080"
if common_http.InternalTLSEnabled() {
url = "https://127.0.0.1:8443"
} }
log.Infof("the factory for adapter %s registered", model.RegistryTypeHarbor)
}
type factory struct {
}
// Create ...
func (f *factory) Create(r *model.Registry) (adp.Adapter, error) {
return newAdapter(r)
}
// AdapterPattern ...
func (f *factory) AdapterPattern() *model.AdapterPattern {
return nil
}
var (
_ adp.Adapter = (*adapter)(nil)
_ adp.ArtifactRegistry = (*adapter)(nil)
_ adp.ChartRegistry = (*adapter)(nil)
)
type adapter struct {
*native.Adapter
registry *model.Registry
url string
client *common_http.Client
}
func newAdapter(registry *model.Registry) (*adapter, error) {
var transport *http.Transport
if registry.URL == config.GetCoreURL() {
transport = common_http.GetHTTPTransport(common_http.SecureTransport)
} else {
transport = util.GetHTTPTransport(registry.Insecure)
}
// local Harbor instance
if registry.Credential != nil && registry.Credential.Type == model.CredentialTypeSecret {
authorizer := common_http_auth.NewSecretAuthorizer(registry.Credential.AccessSecret) authorizer := common_http_auth.NewSecretAuthorizer(registry.Credential.AccessSecret)
return &adapter{ httpClient := common_http.NewClient(&http.Client{
registry: registry, Transport: common_http.GetHTTPTransport(common_http.SecureTransport),
url: registry.URL, }, authorizer)
client: common_http.NewClient( client, err := NewClient(url, httpClient)
&http.Client{ if err != nil {
Transport: transport, return nil, err
}, authorizer), }
return &Adapter{
Adapter: native.NewAdapterWithAuthorizer(registry, authorizer), Adapter: native.NewAdapterWithAuthorizer(registry, authorizer),
Registry: registry,
Client: client,
url: url,
httpClient: httpClient,
}, nil }, nil
} }
@ -96,22 +63,43 @@ func newAdapter(registry *model.Registry) (*adapter, error) {
registry.Credential.AccessKey, registry.Credential.AccessKey,
registry.Credential.AccessSecret)) registry.Credential.AccessSecret))
} }
return &adapter{ httpClient := common_http.NewClient(&http.Client{
registry: registry, Transport: common_http.GetHTTPTransportByInsecure(registry.Insecure),
url: registry.URL, }, authorizers...)
client: common_http.NewClient( client, err := NewClient(registry.URL, httpClient)
&http.Client{ if err != nil {
Transport: transport, return nil, err
}, authorizers...), }
return &Adapter{
Adapter: native.NewAdapter(registry), Adapter: native.NewAdapter(registry),
Registry: registry,
Client: client,
url: registry.URL,
httpClient: httpClient,
}, nil }, nil
} }
func (a *adapter) Info() (*model.RegistryInfo, error) { // Adapter is the base adapter for Harbor
type Adapter struct {
*native.Adapter
Registry *model.Registry
Client *Client
// url and httpClient can be removed if we don't support replicate chartmuseum charts anymore
url string
httpClient *common_http.Client
}
// GetAPIVersion returns the supported API version of the Harbor instance that the adapter is created for
func (a *Adapter) GetAPIVersion() string {
return a.Client.APIVersion
}
// Info provides the information of the Harbor registry instance
func (a *Adapter) Info() (*model.RegistryInfo, error) {
info := &model.RegistryInfo{ info := &model.RegistryInfo{
Type: model.RegistryTypeHarbor, Type: model.RegistryTypeHarbor,
SupportedResourceTypes: []model.ResourceType{ SupportedResourceTypes: []model.ResourceType{
model.ResourceTypeArtifact,
model.ResourceTypeImage, model.ResourceTypeImage,
}, },
SupportedResourceFilters: []*model.FilterStyle{ SupportedResourceFilters: []*model.FilterStyle{
@ -130,40 +118,31 @@ func (a *adapter) Info() (*model.RegistryInfo, error) {
}, },
} }
sys := &struct { enabled, err := a.Client.ChartRegistryEnabled()
ChartRegistryEnabled bool `json:"with_chartmuseum"` if err != nil {
}{}
if err := a.client.Get(fmt.Sprintf("%s/api/%s/systeminfo", a.getURL(), api.APIVersion), sys); err != nil {
return nil, err return nil, err
} }
if sys.ChartRegistryEnabled { if enabled {
info.SupportedResourceTypes = append(info.SupportedResourceTypes, model.ResourceTypeChart) info.SupportedResourceTypes = append(info.SupportedResourceTypes, model.ResourceTypeChart)
} }
labels := []*struct {
Name string `json:"name"` labels, err := a.Client.ListLabels()
}{} if err != nil {
// label isn't supported in some previous version of Harbor
if err := a.client.Get(fmt.Sprintf("%s/api/%s/labels?scope=g", a.getURL(), api.APIVersion), &labels); err != nil {
if e, ok := err.(*common_http.Error); !ok || e.Code != http.StatusNotFound {
return nil, err return nil, err
} }
} else { info.SupportedResourceFilters = append(info.SupportedResourceFilters,
ls := []string{} &model.FilterStyle{
for _, label := range labels {
ls = append(ls, label.Name)
}
labelFilter := &model.FilterStyle{
Type: model.FilterTypeLabel, Type: model.FilterTypeLabel,
Style: model.FilterStyleTypeList, Style: model.FilterStyleTypeList,
Values: ls, Values: labels,
} })
info.SupportedResourceFilters = append(info.SupportedResourceFilters, labelFilter)
}
return info, nil return info, nil
} }
func (a *adapter) PrepareForPush(resources []*model.Resource) error { // PrepareForPush creates projects
projects := map[string]*project{} func (a *Adapter) PrepareForPush(resources []*model.Resource) error {
projects := map[string]*Project{}
for _, resource := range resources { for _, resource := range resources {
if resource == nil { if resource == nil {
return errors.New("the resource cannot be null") return errors.New("the resource cannot be null")
@ -186,21 +165,13 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
if exist { if exist {
metadata = mergeMetadata(pro.Metadata, metadata) metadata = mergeMetadata(pro.Metadata, metadata)
} }
projects[projectName] = &project{ projects[projectName] = &Project{
Name: projectName, Name: projectName,
Metadata: metadata, Metadata: metadata,
} }
} }
for _, project := range projects { for _, project := range projects {
pro := struct { if err := a.Client.CreateProject(project.Name, project.Metadata); err != nil {
Name string `json:"project_name"`
Metadata map[string]interface{} `json:"metadata"`
}{
Name: project.Name,
Metadata: project.Metadata,
}
err := a.client.Post(fmt.Sprintf("%s/api/%s/projects", a.getURL(), api.APIVersion), pro)
if err != nil {
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", project.Name) log.Debugf("got 409 when trying to create project %s", project.Name)
continue continue
@ -212,6 +183,44 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
return nil return nil
} }
// ListProjects lists projects
func (a *Adapter) ListProjects(filters []*model.Filter) ([]*Project, error) {
pattern := ""
for _, filter := range filters {
if filter.Type == model.FilterTypeName {
pattern = filter.Value.(string)
break
}
}
var 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.Client.GetProject(name)
if err != nil {
return nil, err
}
if project == nil {
continue
}
projects = append(projects, project)
}
}
}
if len(projects) > 0 {
var 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.Client.ListProjects("")
}
func abstractPublicMetadata(metadata map[string]interface{}) map[string]interface{} { func abstractPublicMetadata(metadata map[string]interface{}) map[string]interface{} {
if metadata == nil { if metadata == nil {
return nil return nil
@ -257,56 +266,13 @@ func parsePublic(metadata map[string]interface{}) bool {
return false return false
} }
type project struct { // Project model
type Project struct {
ID int64 `json:"project_id"` ID int64 `json:"project_id"`
Name string `json:"name"` Name string `json:"name"`
Metadata map[string]interface{} `json:"metadata"` Metadata map[string]interface{} `json:"metadata"`
} }
func (a *adapter) getProjects(name string) ([]*project, error) { func isLocalHarbor(registry *model.Registry) bool {
projects := []*project{} return registry.Type == model.RegistryTypeHarbor && registry.Name == "Local"
url := fmt.Sprintf("%s/api/%s/projects?name=%s&page=1&page_size=500", a.getURL(), api.APIVersion, name)
if err := a.client.GetAndIteratePagination(url, &projects); err != nil {
return nil, err
}
return projects, nil
}
func (a *adapter) getProject(name string) (*project, error) {
// TODO need an API to exact match project by name
projects, err := a.getProjects(name)
if err != nil {
return nil, err
}
for _, pro := range projects {
if pro.Name == name {
p := &project{
ID: pro.ID,
Name: name,
}
if pro.Metadata != nil {
metadata := map[string]interface{}{}
for key, value := range pro.Metadata {
metadata[key] = value
}
p.Metadata = metadata
}
return p, nil
}
}
return nil, nil
}
// when the adapter is created for local Harbor, returns the "http://127.0.0.1:8080"
// as URL to avoid issue https://github.com/goharbor/harbor-helm/issues/222
// when harbor is deployed on Kubernetes
func (a *adapter) getURL() string {
if a.registry.Type == model.RegistryTypeHarbor && a.registry.Name == "Local" {
if common_http.InternalTLSEnabled() {
return "https://core:8443"
}
return "http://127.0.0.1:8080"
}
return a.url
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package harbor package base
import ( import (
"net/http" "net/http"
@ -25,11 +25,18 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestGetAPIVersion(t *testing.T) {
adapter := &Adapter{
Client: &Client{APIVersion: "1.0"},
}
assert.Equal(t, "1.0", adapter.GetAPIVersion())
}
func TestInfo(t *testing.T) { func TestInfo(t *testing.T) {
// chart museum enabled // chart museum enabled
server := test.NewServer(&test.RequestHandlerMapping{ server := test.NewServer(&test.RequestHandlerMapping{
Method: http.MethodGet, Method: http.MethodGet,
Pattern: "/api/v2.0/systeminfo", Pattern: "/api/systeminfo",
Handler: func(w http.ResponseWriter, r *http.Request) { Handler: func(w http.ResponseWriter, r *http.Request) {
data := `{"with_chartmuseum":true}` data := `{"with_chartmuseum":true}`
w.Write([]byte(data)) w.Write([]byte(data))
@ -38,23 +45,22 @@ func TestInfo(t *testing.T) {
registry := &model.Registry{ registry := &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err := newAdapter(registry) adapter, err := New(registry)
require.Nil(t, err) require.Nil(t, err)
info, err := adapter.Info() info, err := adapter.Info()
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, model.RegistryTypeHarbor, info.Type) assert.Equal(t, model.RegistryTypeHarbor, info.Type)
assert.Equal(t, 2, len(info.SupportedResourceFilters)) assert.Equal(t, 3, len(info.SupportedResourceFilters))
assert.Equal(t, 2, len(info.SupportedTriggers)) assert.Equal(t, 2, len(info.SupportedTriggers))
assert.Equal(t, 3, len(info.SupportedResourceTypes)) assert.Equal(t, 2, len(info.SupportedResourceTypes))
assert.Equal(t, model.ResourceTypeArtifact, info.SupportedResourceTypes[0]) assert.Equal(t, model.ResourceTypeImage, info.SupportedResourceTypes[0])
assert.Equal(t, model.ResourceTypeImage, info.SupportedResourceTypes[1]) assert.Equal(t, model.ResourceTypeChart, info.SupportedResourceTypes[1])
assert.Equal(t, model.ResourceTypeChart, info.SupportedResourceTypes[2])
server.Close() server.Close()
// chart museum disabled // chart museum disabled
server = test.NewServer(&test.RequestHandlerMapping{ server = test.NewServer(&test.RequestHandlerMapping{
Method: http.MethodGet, Method: http.MethodGet,
Pattern: "/api/v2.0/systeminfo", Pattern: "/api/systeminfo",
Handler: func(w http.ResponseWriter, r *http.Request) { Handler: func(w http.ResponseWriter, r *http.Request) {
data := `{"with_chartmuseum":false}` data := `{"with_chartmuseum":false}`
w.Write([]byte(data)) w.Write([]byte(data))
@ -63,23 +69,22 @@ func TestInfo(t *testing.T) {
registry = &model.Registry{ registry = &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err = newAdapter(registry) adapter, err = New(registry)
require.Nil(t, err) require.Nil(t, err)
info, err = adapter.Info() info, err = adapter.Info()
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, model.RegistryTypeHarbor, info.Type) assert.Equal(t, model.RegistryTypeHarbor, info.Type)
assert.Equal(t, 2, len(info.SupportedResourceFilters)) assert.Equal(t, 3, len(info.SupportedResourceFilters))
assert.Equal(t, 2, len(info.SupportedTriggers)) assert.Equal(t, 2, len(info.SupportedTriggers))
assert.Equal(t, 2, len(info.SupportedResourceTypes)) assert.Equal(t, 1, len(info.SupportedResourceTypes))
assert.Equal(t, model.ResourceTypeArtifact, info.SupportedResourceTypes[0]) assert.Equal(t, model.ResourceTypeImage, info.SupportedResourceTypes[0])
assert.Equal(t, model.ResourceTypeImage, info.SupportedResourceTypes[1])
server.Close() server.Close()
} }
func TestPrepareForPush(t *testing.T) { func TestPrepareForPush(t *testing.T) {
server := test.NewServer(&test.RequestHandlerMapping{ server := test.NewServer(&test.RequestHandlerMapping{
Method: http.MethodPost, Method: http.MethodPost,
Pattern: "/api/v2.0/projects", Pattern: "/api/projects",
Handler: func(w http.ResponseWriter, r *http.Request) { Handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
}, },
@ -87,7 +92,7 @@ func TestPrepareForPush(t *testing.T) {
registry := &model.Registry{ registry := &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err := newAdapter(registry) adapter, err := New(registry)
require.Nil(t, err) require.Nil(t, err)
// nil resource // nil resource
err = adapter.PrepareForPush([]*model.Resource{nil}) err = adapter.PrepareForPush([]*model.Resource{nil})
@ -133,7 +138,7 @@ func TestPrepareForPush(t *testing.T) {
// project already exists // project already exists
server = test.NewServer(&test.RequestHandlerMapping{ server = test.NewServer(&test.RequestHandlerMapping{
Method: http.MethodPost, Method: http.MethodPost,
Pattern: "/api/v2.0/projects", Pattern: "/api/projects",
Handler: func(w http.ResponseWriter, r *http.Request) { Handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
}, },
@ -141,7 +146,7 @@ func TestPrepareForPush(t *testing.T) {
registry = &model.Registry{ registry = &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err = newAdapter(registry) adapter, err = New(registry)
require.Nil(t, err) require.Nil(t, err)
err = adapter.PrepareForPush( err = adapter.PrepareForPush(
[]*model.Resource{ []*model.Resource{

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package harbor package base
import ( import (
"bytes" "bytes"
@ -23,9 +23,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/goharbor/harbor/src/replication/filter"
common_http "github.com/goharbor/harbor/src/common/http" common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/replication/filter"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
) )
@ -46,17 +45,18 @@ type chartVersionMetadata struct {
URLs []string `json:"urls"` URLs []string `json:"urls"`
} }
func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error) { // FetchCharts fetches charts
projects, err := a.listProjects(filters) func (a *Adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error) {
projects, err := a.ListProjects(filters)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resources := []*model.Resource{} resources := []*model.Resource{}
for _, project := range projects { for _, project := range projects {
url := fmt.Sprintf("%s/api/chartrepo/%s/charts", a.getURL(), project.Name) url := fmt.Sprintf("%s/api/chartrepo/%s/charts", a.url, project.Name)
repositories := []*model.Repository{} repositories := []*model.Repository{}
if err := a.client.Get(url, &repositories); err != nil { if err := a.httpClient.Get(url, &repositories); err != nil {
return nil, err return nil, err
} }
if len(repositories) == 0 { if len(repositories) == 0 {
@ -72,9 +72,9 @@ func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error
for _, repository := range repositories { for _, repository := range repositories {
name := strings.SplitN(repository.Name, "/", 2)[1] name := strings.SplitN(repository.Name, "/", 2)[1]
url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s", a.getURL(), project.Name, name) url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s", a.url, project.Name, name)
versions := []*chartVersion{} versions := []*chartVersion{}
if err := a.client.Get(url, &versions); err != nil { if err := a.httpClient.Get(url, &versions); err != nil {
return nil, err return nil, err
} }
if len(versions) == 0 { if len(versions) == 0 {
@ -102,7 +102,7 @@ func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error
for _, artifact := range artifacts { for _, artifact := range artifacts {
resources = append(resources, &model.Resource{ resources = append(resources, &model.Resource{
Type: model.ResourceTypeChart, Type: model.ResourceTypeChart,
Registry: a.registry, Registry: a.Registry,
Metadata: &model.ResourceMetadata{ Metadata: &model.ResourceMetadata{
Repository: &model.Repository{ Repository: &model.Repository{
Name: repository.Name, Name: repository.Name,
@ -117,7 +117,8 @@ func (a *adapter) FetchCharts(filters []*model.Filter) ([]*model.Resource, error
return resources, nil return resources, nil
} }
func (a *adapter) ChartExist(name, version string) (bool, error) { // ChartExist checks the existence of the chart
func (a *Adapter) ChartExist(name, version string) (bool, error) {
_, err := a.getChartInfo(name, version) _, err := a.getChartInfo(name, version)
if err == nil { if err == nil {
return true, nil return true, nil
@ -128,20 +129,21 @@ func (a *adapter) ChartExist(name, version string) (bool, error) {
return false, err return false, err
} }
func (a *adapter) getChartInfo(name, version string) (*chartVersionDetail, error) { func (a *Adapter) getChartInfo(name, version string) (*chartVersionDetail, error) {
project, name, err := parseChartName(name) project, name, err := parseChartName(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s/%s", a.url, project, name, version) url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s/%s", a.url, project, name, version)
info := &chartVersionDetail{} info := &chartVersionDetail{}
if err = a.client.Get(url, info); err != nil { if err = a.httpClient.Get(url, info); err != nil {
return nil, err return nil, err
} }
return info, nil return info, nil
} }
func (a *adapter) DownloadChart(name, version string) (io.ReadCloser, error) { // DownloadChart downloads the specific chart
func (a *Adapter) DownloadChart(name, version string) (io.ReadCloser, error) {
info, err := a.getChartInfo(name, version) info, err := a.getChartInfo(name, version)
if err != nil { if err != nil {
return nil, err return nil, err
@ -162,7 +164,7 @@ func (a *adapter) DownloadChart(name, version string) (io.ReadCloser, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
resp, err := a.client.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -176,7 +178,8 @@ func (a *adapter) DownloadChart(name, version string) (io.ReadCloser, error) {
return resp.Body, nil return resp.Body, nil
} }
func (a *adapter) UploadChart(name, version string, chart io.Reader) error { // UploadChart uploads the chart
func (a *Adapter) UploadChart(name, version string, chart io.Reader) error {
project, name, err := parseChartName(name) project, name, err := parseChartName(name)
if err != nil { if err != nil {
return err return err
@ -200,7 +203,7 @@ func (a *adapter) UploadChart(name, version string, chart io.Reader) error {
return err return err
} }
req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := a.client.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -219,13 +222,14 @@ func (a *adapter) UploadChart(name, version string, chart io.Reader) error {
return nil return nil
} }
func (a *adapter) DeleteChart(name, version string) error { // DeleteChart deletes the chart
func (a *Adapter) DeleteChart(name, version string) error {
project, name, err := parseChartName(name) project, name, err := parseChartName(name)
if err != nil { if err != nil {
return err return err
} }
url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s/%s", a.url, project, name, version) url := fmt.Sprintf("%s/api/chartrepo/%s/charts/%s/%s", a.url, project, name, version)
return a.client.Delete(url) return a.httpClient.Delete(url)
} }
// TODO merge this method and utils.ParseRepository? // TODO merge this method and utils.ParseRepository?

View File

@ -12,15 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package harbor package base
import ( import (
"bytes" "bytes"
"fmt"
"net/http" "net/http"
"testing" "testing"
"github.com/goharbor/harbor/src/common/api"
"github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -31,7 +29,7 @@ func TestFetchCharts(t *testing.T) {
server := test.NewServer([]*test.RequestHandlerMapping{ server := test.NewServer([]*test.RequestHandlerMapping{
{ {
Method: http.MethodGet, Method: http.MethodGet,
Pattern: fmt.Sprintf("/api/%s/projects", api.APIVersion), Pattern: "/api/projects",
Handler: func(w http.ResponseWriter, r *http.Request) { Handler: func(w http.ResponseWriter, r *http.Request) {
data := `[{ data := `[{
"name": "library", "name": "library",
@ -69,7 +67,7 @@ func TestFetchCharts(t *testing.T) {
registry := &model.Registry{ registry := &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err := newAdapter(registry) adapter, err := New(registry)
require.Nil(t, err) require.Nil(t, err)
// nil filter // nil filter
resources, err := adapter.FetchCharts(nil) resources, err := adapter.FetchCharts(nil)
@ -116,7 +114,7 @@ func TestChartExist(t *testing.T) {
registry := &model.Registry{ registry := &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err := newAdapter(registry) adapter, err := New(registry)
require.Nil(t, err) require.Nil(t, err)
exist, err := adapter.ChartExist("library/harbor", "1.0") exist, err := adapter.ChartExist("library/harbor", "1.0")
require.Nil(t, err) require.Nil(t, err)
@ -149,7 +147,7 @@ func TestDownloadChart(t *testing.T) {
registry := &model.Registry{ registry := &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err := newAdapter(registry) adapter, err := New(registry)
require.Nil(t, err) require.Nil(t, err)
_, err = adapter.DownloadChart("library/harbor", "1.0") _, err = adapter.DownloadChart("library/harbor", "1.0")
require.Nil(t, err) require.Nil(t, err)
@ -167,7 +165,7 @@ func TestUploadChart(t *testing.T) {
registry := &model.Registry{ registry := &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err := newAdapter(registry) adapter, err := New(registry)
require.Nil(t, err) require.Nil(t, err)
err = adapter.UploadChart("library/harbor", "1.0", bytes.NewBuffer(nil)) err = adapter.UploadChart("library/harbor", "1.0", bytes.NewBuffer(nil))
require.Nil(t, err) require.Nil(t, err)
@ -185,7 +183,7 @@ func TestDeleteChart(t *testing.T) {
registry := &model.Registry{ registry := &model.Registry{
URL: server.URL, URL: server.URL,
} }
adapter, err := newAdapter(registry) adapter, err := New(registry)
require.Nil(t, err) require.Nil(t, err)
err = adapter.DeleteChart("library/harbor", "1.0") err = adapter.DeleteChart("library/harbor", "1.0")
require.Nil(t, err) require.Nil(t, err)

View File

@ -0,0 +1,135 @@
// 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 base
import (
"fmt"
"net/http"
"strings"
common_http "github.com/goharbor/harbor/src/common/http"
)
// NewClient returns an instance of the base client
func NewClient(url string, c *common_http.Client) (*Client, error) {
client := &Client{
URL: strings.TrimSuffix(url, "/"),
C: c,
}
version, err := client.GetAPIVersion()
if err != nil {
return nil, err
}
client.APIVersion = version
return client, nil
}
// Client is the base client that provides common methods for all versions of Harbor clients
type Client struct {
URL string
APIVersion string
C *common_http.Client
}
// GetAPIVersion returns the supported API version
func (c *Client) GetAPIVersion() (string, error) {
version := &struct {
Version string `json:"version"`
}{}
err := c.C.Get(c.URL+"/api/version", version)
if err == nil {
return version.Version, nil
}
// Harbor 1.x has no API version endpoint
if e, ok := err.(*common_http.Error); ok && e.Code == http.StatusNotFound {
return "", nil
}
return "", err
}
// ChartRegistryEnabled returns whether the chart registry is enabled for the Harbor instance
func (c *Client) ChartRegistryEnabled() (bool, error) {
sys := &struct {
ChartRegistryEnabled bool `json:"with_chartmuseum"`
}{}
if err := c.C.Get(c.BaseURL()+"/systeminfo", sys); err != nil {
return false, err
}
return sys.ChartRegistryEnabled, nil
}
// ListLabels lists system level labels
func (c *Client) ListLabels() ([]string, error) {
labels := []*struct {
Name string `json:"name"`
}{}
err := c.C.Get(c.BaseURL()+"/labels?scope=g", &labels)
if err == nil {
var lbs []string
for _, label := range labels {
lbs = append(lbs, label.Name)
}
return lbs, nil
}
// label isn't supported in some previous version of Harbor
if e, ok := err.(*common_http.Error); !ok || e.Code != http.StatusNotFound {
return nil, err
}
return nil, nil
}
// CreateProject creates project
func (c *Client) CreateProject(name string, metadata map[string]interface{}) error {
project := struct {
Name string `json:"project_name"`
Metadata map[string]interface{} `json:"metadata"`
}{
Name: name,
Metadata: metadata,
}
return c.C.Post(c.BaseURL()+"/projects", project)
}
// ListProjects lists projects
func (c *Client) ListProjects(name string) ([]*Project, error) {
projects := []*Project{}
url := fmt.Sprintf("%s/projects?name=%s", c.BaseURL(), name)
if err := c.C.GetAndIteratePagination(url, &projects); err != nil {
return nil, err
}
return projects, nil
}
// GetProject gets the specific project
func (c *Client) GetProject(name string) (*Project, error) {
projects, err := c.ListProjects(name)
if err != nil {
return nil, err
}
for _, project := range projects {
if project.Name == name {
return project, nil
}
}
return nil, nil
}
// BaseURL returns the base URL of APIs
func (c *Client) BaseURL() string {
if len(c.APIVersion) == 0 {
return fmt.Sprintf("%s/api", c.URL)
}
return fmt.Sprintf("%s/api/%s", c.URL, c.APIVersion)
}

View File

@ -1,177 +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/common/api"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/lib/log"
adp "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/filter"
"github.com/goharbor/harbor/src/replication/model"
"github.com/goharbor/harbor/src/replication/util"
"strings"
)
func (a *adapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resource, error) {
projects, err := a.listProjects(filters)
if err != nil {
return nil, err
}
var resources []*model.Resource
for _, project := range projects {
repositories, err := a.listRepositories(project, filters)
if err != nil {
return nil, err
}
if len(repositories) == 0 {
continue
}
var rawResources = make([]*model.Resource, len(repositories))
runner := utils.NewLimitedConcurrentRunner(adp.MaxConcurrency)
defer runner.Cancel()
for i, r := range repositories {
index := i
repo := r
runner.AddTask(func() error {
artifacts, err := a.listArtifacts(repo.Name, filters)
if err != nil {
return fmt.Errorf("failed to list artifacts of repository '%s': %v", repo.Name, err)
}
if len(artifacts) == 0 {
rawResources[index] = nil
return nil
}
rawResources[index] = &model.Resource{
Type: model.ResourceTypeArtifact,
Registry: a.registry,
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: repo.Name,
Metadata: project.Metadata,
},
Artifacts: artifacts,
},
}
return nil
})
}
runner.Wait()
if runner.IsCancelled() {
return nil, fmt.Errorf("FetchArtifacts error when collect tags for repos")
}
for _, r := range rawResources {
if r != nil {
resources = append(resources, r)
}
}
}
return resources, nil
}
func (a *adapter) listProjects(filters []*model.Filter) ([]*project, error) {
pattern := ""
for _, filter := range filters {
if filter.Type == model.FilterTypeName {
pattern = filter.Value.(string)
break
}
}
var 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 {
var 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("")
}
func (a *adapter) listRepositories(project *project, filters []*model.Filter) ([]*model.Repository, error) {
repositories := []*models.RepoRecord{}
url := fmt.Sprintf("%s/api/%s/projects/%s/repositories", a.getURL(), api.APIVersion, project.Name)
if err := a.client.GetAndIteratePagination(url, &repositories); err != nil {
return nil, err
}
var repos []*model.Repository
for _, repository := range repositories {
repos = append(repos, &model.Repository{
Name: repository.Name,
Metadata: project.Metadata,
})
}
return filter.DoFilterRepositories(repos, filters)
}
func (a *adapter) listArtifacts(repository string, filters []*model.Filter) ([]*model.Artifact, error) {
project, repository := utils.ParseRepository(repository)
url := fmt.Sprintf("%s/api/%s/projects/%s/repositories/%s/artifacts?with_label=true",
a.getURL(), api.APIVersion, project, repository)
artifacts := []*artifact.Artifact{}
if err := a.client.GetAndIteratePagination(url, &artifacts); err != nil {
return nil, err
}
var arts []*model.Artifact
for _, artifact := range artifacts {
art := &model.Artifact{
Type: artifact.Type,
Digest: artifact.Digest,
}
for _, label := range artifact.Labels {
art.Labels = append(art.Labels, label.Name)
}
for _, tag := range artifact.Tags {
art.Tags = append(art.Tags, tag.Name)
}
arts = append(arts, art)
}
return filter.DoFilterArtifacts(arts, filters)
}
func (a *adapter) DeleteTag(repository, tag string) error {
project, repository := utils.ParseRepository(repository)
url := fmt.Sprintf("%s/api/%s/projects/%s/repositories/%s/artifacts/%s/tags/%s",
a.getURL(), api.APIVersion, project, repository, tag, tag)
return a.client.Delete(url)
}

View File

@ -1,109 +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 (
"net/http"
"testing"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/replication/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFetchArtifacts(t *testing.T) {
server := test.NewServer([]*test.RequestHandlerMapping{
{
Method: http.MethodGet,
Pattern: "/api/v2.0/projects/library/repositories/hello-world/artifacts",
Handler: func(w http.ResponseWriter, r *http.Request) {
data := `[
{
"digest": "digest1",
"tags": [
{
"name": "1.0"
}
]
},
{
"digest": "digest2",
"tags": [
{
"name": "2.0"
}
]
}
]`
w.Write([]byte(data))
},
},
{
Method: http.MethodGet,
Pattern: "/api/v2.0/projects/library/repositories",
Handler: func(w http.ResponseWriter, r *http.Request) {
data := `[{
"name": "library/hello-world"
}]`
w.Write([]byte(data))
},
},
{
Method: http.MethodGet,
Pattern: "/api/v2.0/projects",
Handler: func(w http.ResponseWriter, r *http.Request) {
data := `[{
"name": "library",
"metadata": {"public":true}
}]`
w.Write([]byte(data))
},
},
}...)
defer server.Close()
registry := &model.Registry{
URL: server.URL,
}
adapter, err := newAdapter(registry)
require.Nil(t, err)
// nil filter
resources, err := adapter.FetchArtifacts(nil)
require.Nil(t, err)
assert.Equal(t, 1, len(resources))
assert.Equal(t, model.ResourceTypeArtifact, resources[0].Type)
assert.Equal(t, "library/hello-world", resources[0].Metadata.Repository.Name)
assert.Equal(t, 2, len(resources[0].Metadata.Artifacts))
assert.Equal(t, "1.0", resources[0].Metadata.Artifacts[0].Tags[0])
assert.Equal(t, "2.0", resources[0].Metadata.Artifacts[1].Tags[0])
// not nil filter
filters := []*model.Filter{
{
Type: model.FilterTypeName,
Value: "library/*",
},
{
Type: model.FilterTypeTag,
Value: "1.0",
},
}
resources, err = adapter.FetchArtifacts(filters)
require.Nil(t, err)
assert.Equal(t, 1, len(resources))
assert.Equal(t, model.ResourceTypeArtifact, resources[0].Type)
assert.Equal(t, "library/hello-world", resources[0].Metadata.Repository.Name)
assert.Equal(t, 1, len(resources[0].Metadata.Artifacts))
assert.Equal(t, "1.0", resources[0].Metadata.Artifacts[0].Tags[0])
}

View File

@ -0,0 +1,126 @@
// 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 v1
import (
"fmt"
"github.com/goharbor/harbor/src/common/utils"
adp "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/adapter/harbor/base"
"github.com/goharbor/harbor/src/replication/filter"
"github.com/goharbor/harbor/src/replication/model"
)
var _ adp.Adapter = &adapter{}
var _ adp.ArtifactRegistry = &adapter{}
var _ adp.ChartRegistry = &adapter{}
// New creates a Adapter for Harbor 1.x
func New(base *base.Adapter) adp.Adapter {
return &adapter{
Adapter: base,
client: &client{Client: base.Client},
}
}
type adapter struct {
*base.Adapter
client *client
}
func (a *adapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resource, error) {
projects, err := a.ListProjects(filters)
if err != nil {
return nil, err
}
var resources []*model.Resource
for _, project := range projects {
repositories, err := a.listRepositories(project, filters)
if err != nil {
return nil, err
}
if len(repositories) == 0 {
continue
}
var rawResources = make([]*model.Resource, len(repositories))
runner := utils.NewLimitedConcurrentRunner(adp.MaxConcurrency)
defer runner.Cancel()
for i, r := range repositories {
index := i
repo := r
runner.AddTask(func() error {
artifacts, err := a.listArtifacts(repo.Name, filters)
if err != nil {
return fmt.Errorf("failed to list artifacts of repository '%s': %v", repo.Name, err)
}
if len(artifacts) == 0 {
rawResources[index] = nil
return nil
}
rawResources[index] = &model.Resource{
Type: model.ResourceTypeImage,
Registry: a.Registry,
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: repo.Name,
Metadata: project.Metadata,
},
Artifacts: artifacts,
},
}
return nil
})
}
runner.Wait()
if runner.IsCancelled() {
return nil, fmt.Errorf("FetchArtifacts error when collect tags for repos")
}
for _, r := range rawResources {
if r != nil {
resources = append(resources, r)
}
}
}
return resources, nil
}
// override the default implementation by calling Harbor API directly
func (a *adapter) DeleteManifest(repository, reference string) error {
return a.client.deleteManifest(repository, reference)
}
func (a *adapter) listRepositories(project *base.Project, filters []*model.Filter) ([]*model.Repository, error) {
repositories, err := a.client.listRepositories(project)
if err != nil {
return nil, err
}
return filter.DoFilterRepositories(repositories, filters)
}
func (a *adapter) listArtifacts(repository string, filters []*model.Filter) ([]*model.Artifact, error) {
artifacts, err := a.client.listArtifacts(repository)
if err != nil {
return nil, err
}
return filter.DoFilterArtifacts(artifacts, filters)
}

View File

@ -0,0 +1,73 @@
// 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 v1
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/replication/adapter/harbor/base"
"github.com/goharbor/harbor/src/replication/model"
)
type client struct {
*base.Client
}
func (c *client) listRepositories(project *base.Project) ([]*model.Repository, error) {
repositories := []*models.RepoRecord{}
url := fmt.Sprintf("%s/repositories?project_id=%d", c.BaseURL(), project.ID)
if err := c.C.GetAndIteratePagination(url, &repositories); err != nil {
return nil, err
}
var repos []*model.Repository
for _, repository := range repositories {
repos = append(repos, &model.Repository{
Name: repository.Name,
Metadata: project.Metadata,
})
}
return repos, nil
}
func (c *client) listArtifacts(repository string) ([]*model.Artifact, error) {
url := fmt.Sprintf("%s/repositories/%s/tags", c.BaseURL(), repository)
tags := []*struct {
Name string `json:"name"`
Labels []*struct {
Name string `json:"name"`
}
}{}
if err := c.C.Get(url, &tags); err != nil {
return nil, err
}
var artifacts []*model.Artifact
for _, tag := range tags {
artifact := &model.Artifact{
Type: string(model.ResourceTypeImage),
Tags: []string{tag.Name},
}
for _, label := range tag.Labels {
artifact.Labels = append(artifact.Labels, label.Name)
}
artifacts = append(artifacts, artifact)
}
return artifacts, nil
}
func (c *client) deleteManifest(repository, reference string) error {
url := fmt.Sprintf("%s/repositories/%s/tags/%s", c.BaseURL(), repository, reference)
return c.C.Delete(url)
}

View File

@ -0,0 +1,134 @@
// 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 v2
import (
"fmt"
"github.com/goharbor/harbor/src/common/utils"
adp "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/adapter/harbor/base"
"github.com/goharbor/harbor/src/replication/filter"
"github.com/goharbor/harbor/src/replication/model"
)
var _ adp.Adapter = &adapter{}
var _ adp.ArtifactRegistry = &adapter{}
var _ adp.ChartRegistry = &adapter{}
// New creates a Adapter for Harbor 2.x
func New(base *base.Adapter) adp.Adapter {
return &adapter{
Adapter: base,
client: &client{Client: base.Client},
}
}
type adapter struct {
*base.Adapter
client *client
}
func (a *adapter) Info() (*model.RegistryInfo, error) {
info, err := a.Adapter.Info()
if err != nil {
return nil, err
}
info.SupportedResourceTypes = append(info.SupportedResourceTypes, model.ResourceTypeArtifact)
return info, err
}
func (a *adapter) FetchArtifacts(filters []*model.Filter) ([]*model.Resource, error) {
projects, err := a.ListProjects(filters)
if err != nil {
return nil, err
}
var resources []*model.Resource
for _, project := range projects {
repositories, err := a.listRepositories(project, filters)
if err != nil {
return nil, err
}
if len(repositories) == 0 {
continue
}
var rawResources = make([]*model.Resource, len(repositories))
runner := utils.NewLimitedConcurrentRunner(adp.MaxConcurrency)
defer runner.Cancel()
for i, r := range repositories {
index := i
repo := r
runner.AddTask(func() error {
artifacts, err := a.listArtifacts(repo.Name, filters)
if err != nil {
return fmt.Errorf("failed to list artifacts of repository '%s': %v", repo.Name, err)
}
if len(artifacts) == 0 {
rawResources[index] = nil
return nil
}
rawResources[index] = &model.Resource{
Type: model.ResourceTypeArtifact,
Registry: a.Registry,
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: repo.Name,
Metadata: project.Metadata,
},
Artifacts: artifacts,
},
}
return nil
})
}
runner.Wait()
if runner.IsCancelled() {
return nil, fmt.Errorf("FetchArtifacts error when collect tags for repos")
}
for _, r := range rawResources {
if r != nil {
resources = append(resources, r)
}
}
}
return resources, nil
}
func (a *adapter) DeleteTag(repository, tag string) error {
return a.client.deleteTag(repository, tag)
}
func (a *adapter) listRepositories(project *base.Project, filters []*model.Filter) ([]*model.Repository, error) {
repositories, err := a.client.listRepositories(project)
if err != nil {
return nil, err
}
return filter.DoFilterRepositories(repositories, filters)
}
func (a *adapter) listArtifacts(repository string, filters []*model.Filter) ([]*model.Artifact, error) {
artifacts, err := a.client.listArtifacts(repository)
if err != nil {
return nil, err
}
return filter.DoFilterArtifacts(artifacts, filters)
}

View File

@ -0,0 +1,80 @@
// 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 v2
import (
"fmt"
"net/url"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/replication/adapter/harbor/base"
"github.com/goharbor/harbor/src/replication/model"
)
type client struct {
*base.Client
}
func (c *client) listRepositories(project *base.Project) ([]*model.Repository, error) {
repositories := []*models.RepoRecord{}
url := fmt.Sprintf("%s/projects/%s/repositories", c.BaseURL(), project.Name)
if err := c.C.GetAndIteratePagination(url, &repositories); err != nil {
return nil, err
}
var repos []*model.Repository
for _, repository := range repositories {
repos = append(repos, &model.Repository{
Name: repository.Name,
Metadata: project.Metadata,
})
}
return repos, nil
}
func (c *client) listArtifacts(repository string) ([]*model.Artifact, error) {
project, repository := utils.ParseRepository(repository)
repository = url.PathEscape(url.PathEscape(repository))
url := fmt.Sprintf("%s/projects/%s/repositories/%s/artifacts?with_label=true",
c.BaseURL(), project, repository)
artifacts := []*artifact.Artifact{}
if err := c.C.GetAndIteratePagination(url, &artifacts); err != nil {
return nil, err
}
var arts []*model.Artifact
for _, artifact := range artifacts {
art := &model.Artifact{
Type: artifact.Type,
Digest: artifact.Digest,
}
for _, label := range artifact.Labels {
art.Labels = append(art.Labels, label.Name)
}
for _, tag := range artifact.Tags {
art.Tags = append(art.Tags, tag.Name)
}
arts = append(arts, art)
}
return arts, nil
}
func (c *client) deleteTag(repository, tag string) error {
project, repository := utils.ParseRepository(repository)
repository = url.PathEscape(url.PathEscape(repository))
url := fmt.Sprintf("%s/projects/%s/repositories/%s/artifacts/%s/tags/%s",
c.BaseURL(), project, repository, tag, tag)
return c.C.Delete(url)
}

View File

@ -24,9 +24,14 @@ import (
"github.com/goharbor/harbor/src/core/service/notifications/jobs" "github.com/goharbor/harbor/src/core/service/notifications/jobs"
"github.com/goharbor/harbor/src/core/service/notifications/scheduler" "github.com/goharbor/harbor/src/core/service/notifications/scheduler"
"github.com/goharbor/harbor/src/core/service/token" "github.com/goharbor/harbor/src/core/service/token"
"github.com/goharbor/harbor/src/server/router"
"net/http"
) )
func registerRoutes() { func registerRoutes() {
// API version
router.NewRoute().Method(http.MethodGet).Path("/api/version").HandlerFunc(GetAPIVersion)
// Controller API: // Controller API:
beego.Router("/c/login", &controllers.CommonController{}, "post:Login") beego.Router("/c/login", &controllers.CommonController{}, "post:Login")
beego.Router("/c/log_out", &controllers.CommonController{}, "get:LogOut") beego.Router("/c/log_out", &controllers.CommonController{}, "get:LogOut")

View File

@ -23,6 +23,7 @@ import (
// RegisterRoutes for Harbor legacy APIs // RegisterRoutes for Harbor legacy APIs
// TODO bump up the version of APIs called by clients // TODO bump up the version of APIs called by clients
func registerLegacyRoutes() { func registerLegacyRoutes() {
version := APIVersion
beego.Router("/api/"+version+"/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{}) beego.Router("/api/"+version+"/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})
beego.Router("/api/"+version+"/projects/", &api.ProjectAPI{}, "head:Head") beego.Router("/api/"+version+"/projects/", &api.ProjectAPI{}, "head:Head")
beego.Router("/api/"+version+"/projects/:id([0-9]+)", &api.ProjectAPI{}) beego.Router("/api/"+version+"/projects/:id([0-9]+)", &api.ProjectAPI{})

View File

@ -20,14 +20,15 @@ import (
"github.com/goharbor/harbor/src/server/v2.0/handler" "github.com/goharbor/harbor/src/server/v2.0/handler"
) )
// const definition
const ( const (
version = "v2.0" APIVersion = "v2.0"
) )
// RegisterRoutes for Harbor v2.0 APIs // RegisterRoutes for Harbor v2.0 APIs
func RegisterRoutes() { func RegisterRoutes() {
registerLegacyRoutes() registerLegacyRoutes()
router.NewRoute().Path("/api/" + version + "/*"). router.NewRoute().Path("/api/" + APIVersion + "/*").
Middleware(apiversion.Middleware(version)). Middleware(apiversion.Middleware(APIVersion)).
Handler(handler.New()) Handler(handler.New())
} }

39
src/server/version.go Normal file
View File

@ -0,0 +1,39 @@
// 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 server
import (
"encoding/json"
"net/http"
serror "github.com/goharbor/harbor/src/server/error"
"github.com/goharbor/harbor/src/server/v2.0/route"
)
var (
version = route.APIVersion
)
// APIVersion model
type APIVersion struct {
Version string `json:"version"`
}
// GetAPIVersion returns the current supported API version
func GetAPIVersion(w http.ResponseWriter, r *http.Request) {
if err := json.NewEncoder(w).Encode(&APIVersion{Version: version}); err != nil {
serror.SendError(w, err)
}
}