mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-27 19:17:47 +01:00
Merge pull request #9329 from chlins/feat/jfrog-docker-registry
feat(replication): support for jfrog artifactory docker image replica…
This commit is contained in:
commit
0a85acac9a
@ -42,6 +42,8 @@ import (
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/azurecr"
|
||||
// register the AliACR adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/aliacr"
|
||||
// register the Jfrog Artifactory adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/jfrog"
|
||||
// register the Helm Hub adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/helmhub"
|
||||
)
|
||||
|
310
src/replication/adapter/jfrog/adapter.go
Normal file
310
src/replication/adapter/jfrog/adapter.go
Normal file
@ -0,0 +1,310 @@
|
||||
package jfrog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
common_http "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/http/modifier"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
||||
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||
"github.com/goharbor/harbor/src/replication/adapter/native"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/goharbor/harbor/src/replication/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := adp.RegisterFactory(model.RegistryTypeJfrogArtifactory, AdapterFactory)
|
||||
if err != nil {
|
||||
log.Errorf("failed to register factory for jfrog artifactory: %v", err)
|
||||
return
|
||||
}
|
||||
log.Infof("the factory of jfrog artifactory adapter was registered")
|
||||
}
|
||||
|
||||
// Adapter is for images replications between harbor and jfrog artifactory image repository
|
||||
type adapter struct {
|
||||
*native.Adapter
|
||||
registry *model.Registry
|
||||
client *common_http.Client
|
||||
}
|
||||
|
||||
var _ adp.Adapter = (*adapter)(nil)
|
||||
|
||||
// Info gets info about jfrog artifactory adapter
|
||||
func (a *adapter) Info() (info *model.RegistryInfo, err error) {
|
||||
info = &model.RegistryInfo{
|
||||
Type: model.RegistryTypeJfrogArtifactory,
|
||||
SupportedResourceTypes: []model.ResourceType{
|
||||
model.ResourceTypeImage,
|
||||
},
|
||||
SupportedResourceFilters: []*model.FilterStyle{
|
||||
{
|
||||
Type: model.FilterTypeName,
|
||||
Style: model.FilterStyleTypeText,
|
||||
},
|
||||
{
|
||||
Type: model.FilterTypeTag,
|
||||
Style: model.FilterStyleTypeText,
|
||||
},
|
||||
},
|
||||
SupportedTriggers: []model.TriggerType{
|
||||
model.TriggerTypeManual,
|
||||
model.TriggerTypeScheduled,
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AdapterFactory is the factory for jfrog artifactory adapter
|
||||
func AdapterFactory(registry *model.Registry) (adp.Adapter, error) {
|
||||
dockerRegistryAdapter, err := native.NewAdapter(registry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
modifiers = []modifier.Modifier{
|
||||
&auth.UserAgentModifier{
|
||||
UserAgent: adp.UserAgentReplication,
|
||||
}}
|
||||
)
|
||||
if registry.Credential != nil {
|
||||
modifiers = append(modifiers, auth.NewBasicAuthCredential(
|
||||
registry.Credential.AccessKey,
|
||||
registry.Credential.AccessSecret))
|
||||
}
|
||||
|
||||
return &adapter{
|
||||
Adapter: dockerRegistryAdapter,
|
||||
registry: registry,
|
||||
client: common_http.NewClient(
|
||||
&http.Client{
|
||||
Transport: util.GetHTTPTransport(registry.Insecure),
|
||||
},
|
||||
modifiers...,
|
||||
),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// PrepareForPush creates local docker repository in jfrog artifactory
|
||||
func (a *adapter) PrepareForPush(resources []*model.Resource) error {
|
||||
var namespaces []string
|
||||
for _, resource := range resources {
|
||||
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.Repository == nil {
|
||||
return errors.New("the namespace of resource cannot be null")
|
||||
}
|
||||
if len(resource.Metadata.Repository.Name) == 0 {
|
||||
return errors.New("the name of namespace cannot be null")
|
||||
}
|
||||
path := strings.Split(resource.Metadata.Repository.Name, "/")
|
||||
if len(path) > 0 {
|
||||
namespaces = append(namespaces, path[0])
|
||||
}
|
||||
}
|
||||
|
||||
repositories, err := a.getLocalRepositories()
|
||||
if err != nil {
|
||||
log.Errorf("Get local repositories error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
existedRepositories := make(map[string]struct{})
|
||||
for _, repo := range repositories {
|
||||
existedRepositories[repo.Key] = struct{}{}
|
||||
}
|
||||
|
||||
for _, namespace := range namespaces {
|
||||
if _, ok := existedRepositories[namespace]; ok {
|
||||
log.Debugf("Namespace %s already existed in remote, skip create it", namespace)
|
||||
} else {
|
||||
err = a.createNamespace(namespace)
|
||||
if err != nil {
|
||||
log.Errorf("Create Namespace %s error: %v", namespace, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *adapter) getLocalRepositories() ([]*repository, error) {
|
||||
var repositories []*repository
|
||||
url := fmt.Sprintf("%s/artifactory/api/repositories?type=local&packageType=docker", a.registry.URL)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return repositories, err
|
||||
}
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return repositories, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return repositories, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &repositories)
|
||||
return repositories, err
|
||||
}
|
||||
|
||||
// create repository with docker local type
|
||||
// this operation needs admin
|
||||
func (a *adapter) createNamespace(namespace string) error {
|
||||
ns := newDefaultDockerLocalRepository(namespace)
|
||||
body, err := json.Marshal(ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/artifactory/api/repositories/%s", a.registry.URL, namespace)
|
||||
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return &common_http.Error{
|
||||
Code: resp.StatusCode,
|
||||
Message: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
// PushBlob can not use naive PushBlob due to MonolithicUpload, Jfrog now just support push by chunk
|
||||
// related issue: https://www.jfrog.com/jira/browse/RTFACT-19344
|
||||
func (a *adapter) PushBlob(repository, digest string, size int64, blob io.Reader) error {
|
||||
location, err := a.preparePushBlob(repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/%s/blobs/uploads/%s", a.registry.URL, repository, location)
|
||||
req, err := http.NewRequest(http.MethodPatch, url, blob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rangeSize := strconv.Itoa(int(size))
|
||||
req.Header.Set("Content-Length", rangeSize)
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("0-%s", rangeSize))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusAccepted {
|
||||
return a.ackPushBlob(repository, digest, location, rangeSize)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return &common_http.Error{
|
||||
Code: resp.StatusCode,
|
||||
Message: string(b),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *adapter) preparePushBlob(repository string) (string, error) {
|
||||
url := fmt.Sprintf("%s/v2/%s/blobs/uploads/", a.registry.URL, repository)
|
||||
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set(http.CanonicalHeaderKey("Content-Length"), "0")
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusAccepted {
|
||||
return resp.Header.Get(http.CanonicalHeaderKey("Docker-Upload-Uuid")), nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = &common_http.Error{
|
||||
Code: resp.StatusCode,
|
||||
Message: string(b),
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (a *adapter) ackPushBlob(repository, digest, location, size string) error {
|
||||
url := fmt.Sprintf("%s/v2/%s/blobs/uploads/%s?digest=%s", a.registry.URL, repository, location, digest)
|
||||
req, err := http.NewRequest(http.MethodPut, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusCreated {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = &common_http.Error{
|
||||
Code: resp.StatusCode,
|
||||
Message: string(b),
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
132
src/replication/adapter/jfrog/adapter_test.go
Normal file
132
src/replication/adapter/jfrog/adapter_test.go
Normal file
@ -0,0 +1,132 @@
|
||||
package jfrog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
adp "github.com/goharbor/harbor/src/replication/adapter"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
fakeUploadID = "ac5fbe00-15f7-4d36-aa0e-cbdcdb15ec75"
|
||||
fakeDigest = "sha256:f0f53b24e58a432aaa333d9993240340"
|
||||
|
||||
fakeNamespace = "mydocker"
|
||||
fakeRepository = "mydocker/nginx"
|
||||
)
|
||||
|
||||
func getMockAdapter(t *testing.T, hasCred, health bool) (*adapter, *httptest.Server) {
|
||||
server := test.NewServer(
|
||||
&test.RequestHandlerMapping{
|
||||
Method: http.MethodGet,
|
||||
Pattern: "/artifactory/api/repositories",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`[
|
||||
{
|
||||
"key": "cyzhang",
|
||||
"description": "",
|
||||
"type": "LOCAL",
|
||||
"url": "http://49.4.2.82:8081/artifactory/cyzhang",
|
||||
"packageType": "Docker"
|
||||
},
|
||||
{
|
||||
"key": "mydocker",
|
||||
"type": "LOCAL",
|
||||
"url": "http://49.4.2.82:8081/artifactory/mydocker",
|
||||
"packageType": "Docker"
|
||||
}
|
||||
]`))
|
||||
},
|
||||
},
|
||||
&test.RequestHandlerMapping{
|
||||
Method: http.MethodPut,
|
||||
Pattern: fmt.Sprintf("/artifactory/api/repositories/%s", fakeNamespace),
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
},
|
||||
},
|
||||
&test.RequestHandlerMapping{
|
||||
Method: http.MethodPost,
|
||||
Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/", fakeRepository),
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Docker-Upload-Uuid", fakeUploadID)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
},
|
||||
},
|
||||
&test.RequestHandlerMapping{
|
||||
Method: http.MethodPatch,
|
||||
Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/%s", fakeRepository, fakeUploadID),
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
},
|
||||
},
|
||||
&test.RequestHandlerMapping{
|
||||
Method: http.MethodPut,
|
||||
Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/%s", fakeRepository, fakeUploadID),
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
registry := &model.Registry{
|
||||
Type: model.RegistryTypeJfrogArtifactory,
|
||||
URL: server.URL,
|
||||
}
|
||||
|
||||
if hasCred {
|
||||
registry.Credential = &model.Credential{
|
||||
AccessKey: "admin",
|
||||
AccessSecret: "password",
|
||||
}
|
||||
}
|
||||
|
||||
factory, err := adp.GetFactory(model.RegistryTypeJfrogArtifactory)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, factory)
|
||||
a, err := factory(registry)
|
||||
|
||||
assert.Nil(t, err)
|
||||
return a.(*adapter), server
|
||||
}
|
||||
|
||||
func TestAdapter_Info(t *testing.T) {
|
||||
a, s := getMockAdapter(t, true, true)
|
||||
defer s.Close()
|
||||
info, err := a.Info()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, info)
|
||||
assert.EqualValues(t, 1, len(info.SupportedResourceTypes))
|
||||
assert.EqualValues(t, model.ResourceTypeImage, info.SupportedResourceTypes[0])
|
||||
}
|
||||
|
||||
func TestAdapter_PrepareForPush(t *testing.T) {
|
||||
a, s := getMockAdapter(t, true, true)
|
||||
defer s.Close()
|
||||
resources := []*model.Resource{
|
||||
{
|
||||
Type: model.ResourceTypeImage,
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: "mydocker/busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := a.PrepareForPush(resources)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestAdapter_PushBlob(t *testing.T) {
|
||||
a, s := getMockAdapter(t, true, true)
|
||||
defer s.Close()
|
||||
err := a.PushBlob(fakeRepository, fakeDigest, 20, bytes.NewReader([]byte("test")))
|
||||
assert.Nil(t, err)
|
||||
}
|
24
src/replication/adapter/jfrog/types.go
Normal file
24
src/replication/adapter/jfrog/types.go
Normal file
@ -0,0 +1,24 @@
|
||||
package jfrog
|
||||
|
||||
type repository struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
PackageType string `json:"packageType"`
|
||||
}
|
||||
|
||||
type repositoryCreate struct {
|
||||
Key string `json:"key"`
|
||||
Rclass string `json:"rclass"`
|
||||
PackageType string `json:"packageType"`
|
||||
RepoLayoutRef string `json:"repoLayoutRef"`
|
||||
}
|
||||
|
||||
func newDefaultDockerLocalRepository(key string) *repositoryCreate {
|
||||
return &repositoryCreate{
|
||||
Key: key,
|
||||
Rclass: "local",
|
||||
PackageType: "docker",
|
||||
RepoLayoutRef: "simple-default",
|
||||
}
|
||||
}
|
@ -22,14 +22,15 @@ import (
|
||||
|
||||
// const definition
|
||||
const (
|
||||
RegistryTypeHarbor RegistryType = "harbor"
|
||||
RegistryTypeDockerHub RegistryType = "docker-hub"
|
||||
RegistryTypeDockerRegistry RegistryType = "docker-registry"
|
||||
RegistryTypeHuawei RegistryType = "huawei-SWR"
|
||||
RegistryTypeGoogleGcr RegistryType = "google-gcr"
|
||||
RegistryTypeAwsEcr RegistryType = "aws-ecr"
|
||||
RegistryTypeAzureAcr RegistryType = "azure-acr"
|
||||
RegistryTypeAliAcr RegistryType = "ali-acr"
|
||||
RegistryTypeHarbor RegistryType = "harbor"
|
||||
RegistryTypeDockerHub RegistryType = "docker-hub"
|
||||
RegistryTypeDockerRegistry RegistryType = "docker-registry"
|
||||
RegistryTypeHuawei RegistryType = "huawei-SWR"
|
||||
RegistryTypeGoogleGcr RegistryType = "google-gcr"
|
||||
RegistryTypeAwsEcr RegistryType = "aws-ecr"
|
||||
RegistryTypeAzureAcr RegistryType = "azure-acr"
|
||||
RegistryTypeAliAcr RegistryType = "ali-acr"
|
||||
RegistryTypeJfrogArtifactory RegistryType = "jfrog-artifactory"
|
||||
|
||||
RegistryTypeHelmHub RegistryType = "helm-hub"
|
||||
|
||||
|
@ -43,6 +43,8 @@ import (
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/azurecr"
|
||||
// register the AliACR adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/aliacr"
|
||||
// register the Jfrog Artifactory adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/jfrog"
|
||||
// register the Helm Hub adapter
|
||||
_ "github.com/goharbor/harbor/src/replication/adapter/helmhub"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user