2019-04-14 08:50:44 +02:00
|
|
|
// 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.
|
|
|
|
|
2019-03-31 17:40:19 +02:00
|
|
|
package native
|
|
|
|
|
|
|
|
import (
|
2019-08-01 03:56:32 +02:00
|
|
|
"context"
|
2019-07-31 17:05:36 +02:00
|
|
|
"fmt"
|
2019-07-01 12:33:55 +02:00
|
|
|
"io"
|
2019-06-29 12:26:46 +02:00
|
|
|
"net/http"
|
2019-07-01 12:33:55 +02:00
|
|
|
"strings"
|
|
|
|
"sync"
|
2019-06-29 12:26:46 +02:00
|
|
|
|
2019-07-01 12:33:55 +02:00
|
|
|
"github.com/docker/distribution"
|
|
|
|
"github.com/docker/distribution/manifest/schema1"
|
|
|
|
"github.com/goharbor/harbor/src/common/http/modifier"
|
|
|
|
common_http_auth "github.com/goharbor/harbor/src/common/http/modifier/auth"
|
2019-07-31 17:05:36 +02:00
|
|
|
"github.com/goharbor/harbor/src/common/utils"
|
2019-03-31 17:40:19 +02:00
|
|
|
"github.com/goharbor/harbor/src/common/utils/log"
|
2019-07-01 12:33:55 +02:00
|
|
|
registry_pkg "github.com/goharbor/harbor/src/common/utils/registry"
|
|
|
|
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
2019-04-12 16:38:56 +02:00
|
|
|
adp "github.com/goharbor/harbor/src/replication/adapter"
|
|
|
|
"github.com/goharbor/harbor/src/replication/model"
|
2019-07-01 12:33:55 +02:00
|
|
|
"github.com/goharbor/harbor/src/replication/util"
|
2019-03-31 17:40:19 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
2019-05-07 06:52:34 +02:00
|
|
|
if err := adp.RegisterFactory(model.RegistryTypeDockerRegistry, func(registry *model.Registry) (adp.Adapter, error) {
|
2019-07-01 12:33:55 +02:00
|
|
|
return NewAdapter(registry)
|
2019-03-31 17:40:19 +02:00
|
|
|
}); err != nil {
|
2019-05-07 06:52:34 +02:00
|
|
|
log.Errorf("failed to register factory for %s: %v", model.RegistryTypeDockerRegistry, err)
|
2019-03-31 17:40:19 +02:00
|
|
|
return
|
|
|
|
}
|
2019-05-07 06:52:34 +02:00
|
|
|
log.Infof("the factory for adapter %s registered", model.RegistryTypeDockerRegistry)
|
2019-03-31 17:40:19 +02:00
|
|
|
}
|
|
|
|
|
2019-07-01 12:33:55 +02:00
|
|
|
var _ adp.Adapter = &Adapter{}
|
|
|
|
|
|
|
|
// Adapter implements an adapter for Docker registry. It can be used to all registries
|
|
|
|
// that implement the registry V2 API
|
|
|
|
type Adapter struct {
|
|
|
|
sync.RWMutex
|
|
|
|
*registry_pkg.Registry
|
|
|
|
registry *model.Registry
|
|
|
|
client *http.Client
|
|
|
|
clients map[string]*registry_pkg.Repository // client for repositories
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewAdapter returns an instance of the Adapter
|
|
|
|
func NewAdapter(registry *model.Registry) (*Adapter, error) {
|
|
|
|
var authorizer modifier.Modifier
|
|
|
|
if registry.Credential != nil && len(registry.Credential.AccessSecret) != 0 {
|
|
|
|
var cred modifier.Modifier
|
|
|
|
if registry.Credential.Type == model.CredentialTypeSecret {
|
|
|
|
cred = common_http_auth.NewSecretAuthorizer(registry.Credential.AccessSecret)
|
|
|
|
} else {
|
|
|
|
cred = auth.NewBasicAuthCredential(
|
|
|
|
registry.Credential.AccessKey,
|
|
|
|
registry.Credential.AccessSecret)
|
|
|
|
}
|
|
|
|
authorizer = auth.NewStandardTokenAuthorizer(&http.Client{
|
|
|
|
Transport: util.GetHTTPTransport(registry.Insecure),
|
|
|
|
}, cred, registry.TokenServiceURL)
|
2019-04-15 10:32:41 +02:00
|
|
|
}
|
2019-07-01 12:33:55 +02:00
|
|
|
return NewAdapterWithCustomizedAuthorizer(registry, authorizer)
|
2019-06-29 12:26:46 +02:00
|
|
|
}
|
|
|
|
|
2019-07-01 12:33:55 +02:00
|
|
|
// NewAdapterWithCustomizedAuthorizer returns an instance of the Adapter with the customized authorizer
|
|
|
|
func NewAdapterWithCustomizedAuthorizer(registry *model.Registry, authorizer modifier.Modifier) (*Adapter, error) {
|
|
|
|
transport := util.GetHTTPTransport(registry.Insecure)
|
|
|
|
modifiers := []modifier.Modifier{
|
|
|
|
&auth.UserAgentModifier{
|
|
|
|
UserAgent: adp.UserAgentReplication,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if authorizer != nil {
|
|
|
|
modifiers = append(modifiers, authorizer)
|
|
|
|
}
|
|
|
|
client := &http.Client{
|
|
|
|
Transport: registry_pkg.NewTransport(transport, modifiers...),
|
|
|
|
}
|
|
|
|
reg, err := registry_pkg.NewRegistry(registry.URL, client)
|
2019-06-29 12:26:46 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-07-01 12:33:55 +02:00
|
|
|
return &Adapter{
|
|
|
|
Registry: reg,
|
|
|
|
registry: registry,
|
|
|
|
client: client,
|
|
|
|
clients: map[string]*registry_pkg.Repository{},
|
2019-04-15 10:32:41 +02:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-07-01 12:33:55 +02:00
|
|
|
// Info returns the basic information about the adapter
|
|
|
|
func (a *Adapter) Info() (info *model.RegistryInfo, err error) {
|
2019-03-31 17:40:19 +02:00
|
|
|
return &model.RegistryInfo{
|
2019-05-07 06:52:34 +02:00
|
|
|
Type: model.RegistryTypeDockerRegistry,
|
2019-03-31 17:40:19 +02:00
|
|
|
SupportedResourceTypes: []model.ResourceType{
|
2019-04-17 10:04:54 +02:00
|
|
|
model.ResourceTypeImage,
|
2019-03-31 17:40:19 +02:00
|
|
|
},
|
|
|
|
SupportedResourceFilters: []*model.FilterStyle{
|
|
|
|
{
|
|
|
|
Type: model.FilterTypeName,
|
|
|
|
Style: model.FilterStyleTypeText,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Type: model.FilterTypeTag,
|
|
|
|
Style: model.FilterStyleTypeText,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
SupportedTriggers: []model.TriggerType{
|
|
|
|
model.TriggerTypeManual,
|
|
|
|
model.TriggerTypeScheduled,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-07-01 12:33:55 +02:00
|
|
|
// PrepareForPush does nothing
|
|
|
|
func (a *Adapter) PrepareForPush([]*model.Resource) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// HealthCheck checks health status of a registry
|
|
|
|
func (a *Adapter) HealthCheck() (model.HealthStatus, error) {
|
|
|
|
var err error
|
|
|
|
if a.registry.Credential == nil ||
|
|
|
|
(len(a.registry.Credential.AccessKey) == 0 && len(a.registry.Credential.AccessSecret) == 0) {
|
|
|
|
err = a.PingSimple()
|
|
|
|
} else {
|
|
|
|
err = a.Ping()
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("failed to ping registry %s: %v", a.registry.URL, err)
|
|
|
|
return model.Unhealthy, nil
|
|
|
|
}
|
|
|
|
return model.Healthy, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// FetchImages ...
|
|
|
|
func (a *Adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error) {
|
|
|
|
repositories, err := a.getRepositories(filters)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(repositories) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
for _, filter := range filters {
|
|
|
|
if err = filter.DoFilter(&repositories); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-31 17:05:36 +02:00
|
|
|
rawResources := make([]*model.Resource, len(repositories))
|
|
|
|
var wg = new(sync.WaitGroup)
|
2019-08-01 03:56:32 +02:00
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
var passportsPool = utils.NewPassportsPool(adp.MaxConcurrency, ctx.Done())
|
2019-07-31 17:05:36 +02:00
|
|
|
|
|
|
|
for i, r := range repositories {
|
|
|
|
wg.Add(1)
|
|
|
|
go func(index int, repo *adp.Repository) {
|
|
|
|
defer func() {
|
|
|
|
wg.Done()
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Return false means no passport acquired, and no valid passport will be dispatched any more.
|
|
|
|
// For example, some crucial errors happened and all tasks should be cancelled.
|
|
|
|
if ok := passportsPool.Apply(); !ok {
|
|
|
|
return
|
2019-07-01 12:33:55 +02:00
|
|
|
}
|
2019-07-31 17:05:36 +02:00
|
|
|
defer func() {
|
|
|
|
passportsPool.Revoke()
|
|
|
|
}()
|
|
|
|
|
|
|
|
vTags, err := a.getVTags(repo.Name)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("List tags for repo '%s' error: %v", repo.Name, err)
|
2019-08-01 03:56:32 +02:00
|
|
|
cancel()
|
2019-07-31 17:05:36 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(vTags) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, filter := range filters {
|
|
|
|
if err = filter.DoFilter(&vTags); err != nil {
|
|
|
|
log.Errorf("Filter tags %v error: %v", vTags, err)
|
2019-08-01 03:56:32 +02:00
|
|
|
cancel()
|
2019-07-31 17:05:36 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(vTags) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tags := []string{}
|
|
|
|
for _, vTag := range vTags {
|
|
|
|
tags = append(tags, vTag.Name)
|
|
|
|
}
|
|
|
|
rawResources[index] = &model.Resource{
|
|
|
|
Type: model.ResourceTypeImage,
|
|
|
|
Registry: a.registry,
|
|
|
|
Metadata: &model.ResourceMetadata{
|
|
|
|
Repository: &model.Repository{
|
|
|
|
Name: repo.Name,
|
|
|
|
},
|
|
|
|
Vtags: tags,
|
2019-07-01 12:33:55 +02:00
|
|
|
},
|
2019-07-31 17:05:36 +02:00
|
|
|
}
|
|
|
|
}(i, r)
|
|
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
|
2019-08-01 03:56:32 +02:00
|
|
|
err = ctx.Err()
|
|
|
|
cancel()
|
|
|
|
if err != nil {
|
2019-07-31 17:05:36 +02:00
|
|
|
return nil, fmt.Errorf("FetchImages error when collect tags for repos")
|
|
|
|
}
|
|
|
|
|
|
|
|
var resources []*model.Resource
|
|
|
|
for _, r := range rawResources {
|
|
|
|
if r != nil {
|
|
|
|
resources = append(resources, r)
|
|
|
|
}
|
2019-07-01 12:33:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return resources, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Adapter) getRepositories(filters []*model.Filter) ([]*adp.Repository, error) {
|
|
|
|
pattern := ""
|
|
|
|
for _, filter := range filters {
|
|
|
|
if filter.Type == model.FilterTypeName {
|
|
|
|
pattern = filter.Value.(string)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var repositories []string
|
|
|
|
var err error
|
|
|
|
// if the pattern of repository name filter is a specific repository name, just returns
|
|
|
|
// the parsed repositories and will check the existence later when filtering the tags
|
|
|
|
if paths, ok := util.IsSpecificPath(pattern); ok {
|
|
|
|
repositories = paths
|
|
|
|
} else {
|
|
|
|
// search repositories from catalog API
|
|
|
|
repositories, err = a.Catalog()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
result := []*adp.Repository{}
|
|
|
|
for _, repository := range repositories {
|
|
|
|
result = append(result, &adp.Repository{
|
|
|
|
ResourceType: string(model.ResourceTypeImage),
|
|
|
|
Name: repository,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Adapter) getVTags(repository string) ([]*adp.VTag, error) {
|
|
|
|
tags, err := a.ListTag(repository)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var result []*adp.VTag
|
|
|
|
for _, tag := range tags {
|
|
|
|
result = append(result, &adp.VTag{
|
|
|
|
ResourceType: string(model.ResourceTypeImage),
|
|
|
|
Name: tag,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ManifestExist ...
|
|
|
|
func (a *Adapter) ManifestExist(repository, reference string) (bool, string, error) {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return false, "", err
|
|
|
|
}
|
|
|
|
digest, exist, err := client.ManifestExist(reference)
|
|
|
|
return exist, digest, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// PullManifest ...
|
|
|
|
func (a *Adapter) PullManifest(repository, reference string, accepttedMediaTypes []string) (distribution.Manifest, string, error) {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
digest, mediaType, payload, err := client.PullManifest(reference, accepttedMediaTypes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
if strings.Contains(mediaType, "application/json") {
|
|
|
|
mediaType = schema1.MediaTypeManifest
|
|
|
|
}
|
|
|
|
manifest, _, err := registry_pkg.UnMarshal(mediaType, payload)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
return manifest, digest, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PushManifest ...
|
|
|
|
func (a *Adapter) PushManifest(repository, reference, mediaType string, payload []byte) error {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = client.PushManifest(reference, mediaType, payload)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteManifest ...
|
|
|
|
func (a *Adapter) DeleteManifest(repository, reference string) error {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
digest := reference
|
|
|
|
if !isDigest(digest) {
|
|
|
|
dgt, exist, err := client.ManifestExist(reference)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !exist {
|
|
|
|
log.Debugf("the manifest of %s:%s doesn't exist", repository, reference)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
digest = dgt
|
|
|
|
}
|
|
|
|
return client.DeleteManifest(digest)
|
|
|
|
}
|
|
|
|
|
|
|
|
// BlobExist ...
|
|
|
|
func (a *Adapter) BlobExist(repository, digest string) (bool, error) {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return client.BlobExist(digest)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PullBlob ...
|
|
|
|
func (a *Adapter) PullBlob(repository, digest string) (int64, io.ReadCloser, error) {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
return client.PullBlob(digest)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PushBlob ...
|
|
|
|
func (a *Adapter) PushBlob(repository, digest string, size int64, blob io.Reader) error {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return client.PushBlob(digest, size, blob)
|
|
|
|
}
|
|
|
|
|
|
|
|
func isDigest(str string) bool {
|
|
|
|
return strings.Contains(str, ":")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListTag ...
|
|
|
|
func (a *Adapter) ListTag(repository string) ([]string, error) {
|
|
|
|
client, err := a.getClient(repository)
|
|
|
|
if err != nil {
|
|
|
|
return []string{}, err
|
|
|
|
}
|
|
|
|
return client.ListTag()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Adapter) getClient(repository string) (*registry_pkg.Repository, error) {
|
|
|
|
a.RLock()
|
|
|
|
client, exist := a.clients[repository]
|
|
|
|
a.RUnlock()
|
|
|
|
if exist {
|
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return a.create(repository)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Adapter) create(repository string) (*registry_pkg.Repository, error) {
|
|
|
|
a.Lock()
|
|
|
|
defer a.Unlock()
|
|
|
|
// double check
|
|
|
|
client, exist := a.clients[repository]
|
|
|
|
if exist {
|
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
client, err := registry_pkg.NewRepository(repository, a.registry.URL, a.client)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
a.clients[repository] = client
|
|
|
|
return client, nil
|
|
|
|
}
|