Merge pull request #9329 from chlins/feat/jfrog-docker-registry

feat(replication): support for jfrog artifactory docker image replica…
This commit is contained in:
Wenkai Yin(尹文开) 2019-10-10 09:41:41 +08:00 committed by GitHub
commit 0a85acac9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 479 additions and 8 deletions

View File

@ -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"
)

View 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
}

View 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)
}

View 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",
}
}

View File

@ -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"

View File

@ -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"
)