mirror of https://github.com/goharbor/harbor.git
760 lines
22 KiB
Go
760 lines
22 KiB
Go
// 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 registry
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/manifest/manifestlist"
|
|
_ "github.com/docker/distribution/manifest/ocischema" // register oci manifest unmarshal function
|
|
"github.com/docker/distribution/manifest/schema1"
|
|
"github.com/docker/distribution/manifest/schema2"
|
|
"github.com/opencontainers/go-digest"
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
commonhttp "github.com/goharbor/harbor/src/common/http"
|
|
"github.com/goharbor/harbor/src/lib"
|
|
"github.com/goharbor/harbor/src/lib/config"
|
|
"github.com/goharbor/harbor/src/lib/errors"
|
|
"github.com/goharbor/harbor/src/lib/log"
|
|
"github.com/goharbor/harbor/src/pkg/registry/auth"
|
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor"
|
|
"github.com/goharbor/harbor/src/pkg/registry/interceptor/readonly"
|
|
)
|
|
|
|
var (
|
|
// Cli is the global registry client instance, it targets to the backend docker registry
|
|
Cli = func() Client {
|
|
url, _ := config.RegistryURL()
|
|
username, password := config.RegistryCredential()
|
|
return NewClient(url, username, password, false, readonly.NewInterceptor())
|
|
}()
|
|
|
|
accepts = []string{
|
|
v1.MediaTypeImageIndex,
|
|
manifestlist.MediaTypeManifestList,
|
|
v1.MediaTypeImageManifest,
|
|
schema2.MediaTypeManifest,
|
|
schema1.MediaTypeSignedManifest,
|
|
schema1.MediaTypeManifest,
|
|
}
|
|
)
|
|
|
|
// const definition
|
|
const (
|
|
UserAgent = "harbor-registry-client"
|
|
// DefaultHTTPClientTimeout is the default timeout for registry http client.
|
|
DefaultHTTPClientTimeout = 30 * time.Minute
|
|
)
|
|
|
|
var (
|
|
// registryHTTPClientTimeout is the timeout for registry http client.
|
|
registryHTTPClientTimeout time.Duration
|
|
)
|
|
|
|
func init() {
|
|
registryHTTPClientTimeout = DefaultHTTPClientTimeout
|
|
// override it if read from environment variable, in minutes
|
|
if env := os.Getenv("REGISTRY_HTTP_CLIENT_TIMEOUT"); len(env) > 0 {
|
|
timeout, err := strconv.ParseInt(env, 10, 64)
|
|
if err != nil {
|
|
log.Errorf("Failed to parse REGISTRY_HTTP_CLIENT_TIMEOUT: %v, use default value: %v", err, DefaultHTTPClientTimeout)
|
|
} else {
|
|
if timeout > 0 {
|
|
registryHTTPClientTimeout = time.Duration(timeout) * time.Minute
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Client defines the methods that a registry client should implements
|
|
type Client interface {
|
|
// Ping the base API endpoint "/v2/"
|
|
Ping() (err error)
|
|
// Catalog the repositories
|
|
Catalog() (repositories []string, err error)
|
|
// ListTags lists the tags under the specified repository
|
|
ListTags(repository string) (tags []string, err error)
|
|
// ManifestExist checks the existence of the manifest
|
|
ManifestExist(repository, reference string) (exist bool, desc *distribution.Descriptor, err error)
|
|
// PullManifest pulls the specified manifest
|
|
PullManifest(repository, reference string, acceptedMediaTypes ...string) (manifest distribution.Manifest, digest string, err error)
|
|
// PushManifest pushes the specified manifest
|
|
PushManifest(repository, reference, mediaType string, payload []byte) (digest string, err error)
|
|
// DeleteManifest deletes the specified manifest. The "reference" can be "tag" or "digest"
|
|
DeleteManifest(repository, reference string) (err error)
|
|
// BlobExist checks the existence of the specified blob
|
|
BlobExist(repository, digest string) (exist bool, err error)
|
|
// PullBlob pulls the specified blob. The caller must close the returned "blob"
|
|
PullBlob(repository, digest string) (size int64, blob io.ReadCloser, err error)
|
|
// PullBlobChunk pulls the specified blob, but by chunked
|
|
PullBlobChunk(repository, digest string, blobSize, start, end int64) (size int64, blob io.ReadCloser, err error)
|
|
// PushBlob pushes the specified blob
|
|
PushBlob(repository, digest string, size int64, blob io.Reader) error
|
|
// PushBlobChunk pushes the specified blob, but by chunked
|
|
PushBlobChunk(repository, digest string, blobSize int64, chunk io.Reader, start, end int64, location string) (nextUploadLocation string, endRange int64, err error)
|
|
// MountBlob mounts the blob from the source repository
|
|
MountBlob(srcRepository, digest, dstRepository string) (err error)
|
|
// DeleteBlob deletes the specified blob
|
|
DeleteBlob(repository, digest string) (err error)
|
|
// Copy the artifact from source repository to the destination. The "override"
|
|
// is used to specify whether the destination artifact will be overridden if
|
|
// its name is same with source but digest isn't
|
|
Copy(srcRepository, srcReference, dstRepository, dstReference string, override bool) (err error)
|
|
// Do send generic HTTP requests to the target registry service
|
|
Do(req *http.Request) (*http.Response, error)
|
|
}
|
|
|
|
// NewClient creates a registry client with the default authorizer which determines the auth scheme
|
|
// of the registry automatically and calls the corresponding underlying authorizers(basic/bearer) to
|
|
// do the auth work. If a customized authorizer is needed, use "NewClientWithAuthorizer" instead
|
|
func NewClient(url, username, password string, insecure bool, interceptors ...interceptor.Interceptor) Client {
|
|
authorizer := auth.NewAuthorizer(username, password, insecure)
|
|
return NewClientWithAuthorizer(url, authorizer, insecure, interceptors...)
|
|
}
|
|
|
|
// NewClientWithAuthorizer creates a registry client with the provided authorizer
|
|
func NewClientWithAuthorizer(url string, authorizer lib.Authorizer, insecure bool, interceptors ...interceptor.Interceptor) Client {
|
|
return &client{
|
|
url: url,
|
|
authorizer: authorizer,
|
|
interceptors: interceptors,
|
|
client: &http.Client{
|
|
Transport: commonhttp.GetHTTPTransport(commonhttp.WithInsecure(insecure)),
|
|
Timeout: registryHTTPClientTimeout,
|
|
},
|
|
}
|
|
}
|
|
|
|
type client struct {
|
|
url string
|
|
authorizer lib.Authorizer
|
|
interceptors []interceptor.Interceptor
|
|
client *http.Client
|
|
}
|
|
|
|
func (c *client) Ping() error {
|
|
req, err := http.NewRequest(http.MethodGet, buildPingURL(c.url), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *client) Catalog() ([]string, error) {
|
|
var repositories []string
|
|
url := buildCatalogURL(c.url)
|
|
for {
|
|
repos, next, err := c.catalog(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repositories = append(repositories, repos...)
|
|
|
|
url = next
|
|
// no next page, end the loop
|
|
if len(url) == 0 {
|
|
break
|
|
}
|
|
// relative URL
|
|
if !strings.Contains(url, "://") {
|
|
url = c.url + url
|
|
}
|
|
}
|
|
return repositories, nil
|
|
}
|
|
|
|
func (c *client) catalog(url string) ([]string, string, error) {
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
repositories := struct {
|
|
Repositories []string `json:"repositories"`
|
|
}{}
|
|
if err := json.Unmarshal(body, &repositories); err != nil {
|
|
return nil, "", err
|
|
}
|
|
return repositories.Repositories, next(resp.Header.Get("Link")), nil
|
|
}
|
|
|
|
func (c *client) ListTags(repository string) ([]string, error) {
|
|
var tags []string
|
|
url := buildTagListURL(c.url, repository)
|
|
for {
|
|
tgs, next, err := c.listTags(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tags = append(tags, tgs...)
|
|
|
|
url = next
|
|
// no next page, end the loop
|
|
if len(url) == 0 {
|
|
break
|
|
}
|
|
// relative URL
|
|
if !strings.Contains(url, "://") {
|
|
url = c.url + url
|
|
}
|
|
}
|
|
return tags, nil
|
|
}
|
|
|
|
func (c *client) listTags(url string) ([]string, string, error) {
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
tgs := struct {
|
|
Tags []string `json:"tags"`
|
|
}{}
|
|
if err := json.Unmarshal(body, &tgs); err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tgs.Tags, next(resp.Header.Get("Link")), nil
|
|
}
|
|
|
|
func (c *client) ManifestExist(repository, reference string) (bool, *distribution.Descriptor, error) {
|
|
req, err := http.NewRequest(http.MethodHead, buildManifestURL(c.url, repository, reference), nil)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
for _, mediaType := range accepts {
|
|
req.Header.Add("Accept", mediaType)
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
if errors.IsErr(err, errors.NotFoundCode) {
|
|
return false, nil, nil
|
|
}
|
|
return false, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
dig := resp.Header.Get("Docker-Content-Digest")
|
|
contentType := resp.Header.Get("Content-Type")
|
|
contentLen := resp.Header.Get("Content-Length")
|
|
lenth, _ := strconv.Atoi(contentLen)
|
|
return true, &distribution.Descriptor{Digest: digest.Digest(dig), MediaType: contentType, Size: int64(lenth)}, nil
|
|
}
|
|
|
|
func (c *client) PullManifest(repository, reference string, acceptedMediaTypes ...string) (
|
|
distribution.Manifest, string, error) {
|
|
req, err := http.NewRequest(http.MethodGet, buildManifestURL(c.url, repository, reference), nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(acceptedMediaTypes) == 0 {
|
|
acceptedMediaTypes = accepts
|
|
}
|
|
for _, mediaType := range acceptedMediaTypes {
|
|
req.Header.Add("Accept", mediaType)
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
payload, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
mediaType := resp.Header.Get("Content-Type")
|
|
manifest, _, err := distribution.UnmarshalManifest(mediaType, payload)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
digest := resp.Header.Get("Docker-Content-Digest")
|
|
return manifest, digest, nil
|
|
}
|
|
|
|
func (c *client) PushManifest(repository, reference, mediaType string, payload []byte) (string, error) {
|
|
req, err := http.NewRequest(http.MethodPut, buildManifestURL(c.url, repository, reference),
|
|
bytes.NewReader(payload))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Content-Type", mediaType)
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
return resp.Header.Get("Docker-Content-Digest"), nil
|
|
}
|
|
|
|
func (c *client) DeleteManifest(repository, reference string) error {
|
|
_, err := digest.Parse(reference)
|
|
if err != nil {
|
|
// the reference is tag, get the digest first
|
|
exist, desc, err := c.ManifestExist(repository, reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exist {
|
|
return errors.New(nil).WithCode(errors.NotFoundCode).
|
|
WithMessage("%s:%s not found", repository, reference)
|
|
}
|
|
reference = string(desc.Digest)
|
|
}
|
|
req, err := http.NewRequest(http.MethodDelete, buildManifestURL(c.url, repository, reference), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *client) BlobExist(repository, digest string) (bool, error) {
|
|
req, err := http.NewRequest(http.MethodHead, buildBlobURL(c.url, repository, digest), nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
if errors.IsErr(err, errors.NotFoundCode) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
defer resp.Body.Close()
|
|
return true, nil
|
|
}
|
|
|
|
func (c *client) PullBlob(repository, digest string) (int64, io.ReadCloser, error) {
|
|
req, err := http.NewRequest(http.MethodGet, buildBlobURL(c.url, repository, digest), nil)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
req.Header.Add("Accept-Encoding", "identity")
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
var size int64
|
|
n := resp.Header.Get("Content-Length")
|
|
// no content-length is acceptable, which can taken from manifests
|
|
if len(n) > 0 {
|
|
size, err = strconv.ParseInt(n, 10, 64)
|
|
if err != nil {
|
|
defer resp.Body.Close()
|
|
return 0, nil, err
|
|
}
|
|
}
|
|
|
|
return size, resp.Body, nil
|
|
}
|
|
|
|
// PullBlobChunk pulls the specified blob, but by chunked, refer to https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull for more details.
|
|
func (c *client) PullBlobChunk(repository, digest string, _ int64, start, end int64) (int64, io.ReadCloser, error) {
|
|
req, err := http.NewRequest(http.MethodGet, buildBlobURL(c.url, repository, digest), nil)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
req.Header.Add("Accept-Encoding", "identity")
|
|
req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", start, end))
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
var size int64
|
|
n := resp.Header.Get("Content-Length")
|
|
// no content-length is acceptable, which can taken from manifests
|
|
if len(n) > 0 {
|
|
size, err = strconv.ParseInt(n, 10, 64)
|
|
if err != nil {
|
|
defer resp.Body.Close()
|
|
return 0, nil, err
|
|
}
|
|
}
|
|
|
|
return size, resp.Body, nil
|
|
}
|
|
|
|
func (c *client) PushBlob(repository, digest string, size int64, blob io.Reader) error {
|
|
location, _, err := c.initiateBlobUpload(repository)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.monolithicBlobUpload(location, digest, size, blob)
|
|
}
|
|
|
|
// PushBlobChunk pushes the specified blob, but by chunked, refer to https://github.com/opencontainers/distribution-spec/blob/main/spec.md#push for more details.
|
|
func (c *client) PushBlobChunk(repository, digest string, blobSize int64, chunk io.Reader, start, end int64, location string) (string, int64, error) {
|
|
var err error
|
|
// first chunk need to initialize blob upload location
|
|
if start == 0 {
|
|
location, _, err = c.initiateBlobUpload(repository)
|
|
if err != nil {
|
|
return location, end, err
|
|
}
|
|
}
|
|
|
|
// the range is from 0 to (blobSize-1), so (end == blobSize-1) means it is last chunk
|
|
lastChunk := end == blobSize-1
|
|
url, err := buildChunkBlobUploadURL(c.url, location, digest, lastChunk)
|
|
if err != nil {
|
|
return location, end, err
|
|
}
|
|
|
|
// use PUT instead of PATCH for last chunk which can reduce a final request
|
|
method := http.MethodPatch
|
|
if lastChunk {
|
|
method = http.MethodPut
|
|
}
|
|
req, err := http.NewRequest(method, url, chunk)
|
|
if err != nil {
|
|
return location, end, err
|
|
}
|
|
|
|
req.Header.Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
|
req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", start, end))
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
// if push chunk error, we should query the upload progress for new location and end range.
|
|
newLocation, newEnd, err1 := c.getUploadStatus(location)
|
|
if err1 == nil {
|
|
return newLocation, newEnd, err
|
|
}
|
|
// end should return start-1 to re-push this chunk
|
|
return location, start - 1, fmt.Errorf("failed to get upload status: %w", err1)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
// return the location for next chunk upload
|
|
return resp.Header.Get("Location"), end, nil
|
|
}
|
|
|
|
func (c *client) getUploadStatus(location string) (string, int64, error) {
|
|
req, err := http.NewRequest(http.MethodGet, location, nil)
|
|
if err != nil {
|
|
return location, -1, err
|
|
}
|
|
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return location, -1, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
_, end, err := parseContentRange(resp.Header.Get("Range"))
|
|
if err != nil {
|
|
return location, -1, err
|
|
}
|
|
|
|
return resp.Header.Get("Location"), end, nil
|
|
}
|
|
|
|
func parseContentRange(cr string) (int64, int64, error) {
|
|
ranges := strings.Split(cr, "-")
|
|
if len(ranges) != 2 {
|
|
return -1, -1, fmt.Errorf("invalid content range format, %s", cr)
|
|
}
|
|
start, err := strconv.ParseInt(ranges[0], 10, 64)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
end, err := strconv.ParseInt(ranges[1], 10, 64)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
|
|
return start, end, nil
|
|
}
|
|
|
|
func (c *client) initiateBlobUpload(repository string) (string, string, error) {
|
|
req, err := http.NewRequest(http.MethodPost, buildInitiateBlobUploadURL(c.url, repository), nil)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
req.Header.Set("Content-Length", "0")
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
return resp.Header.Get("Location"), resp.Header.Get("Docker-Upload-UUID"), nil
|
|
}
|
|
|
|
func (c *client) monolithicBlobUpload(location, digest string, size int64, data io.Reader) error {
|
|
url, err := buildMonolithicBlobUploadURL(c.url, location, digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest(http.MethodPut, url, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.ContentLength = size
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *client) MountBlob(srcRepository, digest, dstRepository string) error {
|
|
req, err := http.NewRequest(http.MethodPost, buildMountBlobURL(c.url, dstRepository, digest, srcRepository), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Length", "0")
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *client) DeleteBlob(repository, digest string) error {
|
|
req, err := http.NewRequest(http.MethodDelete, buildBlobURL(c.url, repository, digest), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *client) Copy(srcRepo, srcRef, dstRepo, dstRef string, override bool) error {
|
|
// pull the manifest from the source repository
|
|
manifest, srcDgt, err := c.PullManifest(srcRepo, srcRef)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check the existence of the artifact on the destination repository
|
|
exist, desc, err := c.ManifestExist(dstRepo, dstRef)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exist {
|
|
// the same artifact already exists
|
|
if desc != nil && srcDgt == string(desc.Digest) {
|
|
return nil
|
|
}
|
|
// the same name artifact exists, but not allowed to override
|
|
if !override {
|
|
return errors.New(nil).WithCode(errors.PreconditionCode).
|
|
WithMessage("the same name but different digest artifact exists, but the override is set to false")
|
|
}
|
|
}
|
|
|
|
for _, descriptor := range manifest.References() {
|
|
digest := descriptor.Digest.String()
|
|
switch descriptor.MediaType {
|
|
// skip foreign layer
|
|
case schema2.MediaTypeForeignLayer:
|
|
continue
|
|
// manifest or index
|
|
case v1.MediaTypeImageIndex, manifestlist.MediaTypeManifestList,
|
|
v1.MediaTypeImageManifest, schema2.MediaTypeManifest,
|
|
schema1.MediaTypeSignedManifest, schema1.MediaTypeManifest:
|
|
if err = c.Copy(srcRepo, digest, dstRepo, digest, false); err != nil {
|
|
return err
|
|
}
|
|
// common layer
|
|
default:
|
|
exist, err := c.BlobExist(dstRepo, digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// the layer already exist, skip
|
|
if exist {
|
|
continue
|
|
}
|
|
// when the copy happens inside the same registry, use mount
|
|
if err = c.MountBlob(srcRepo, digest, dstRepo); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
mediaType, payload, err := manifest.Payload()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// push manifest to the destination repository
|
|
if _, err = c.PushManifest(dstRepo, dstRef, mediaType, payload); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *client) Do(req *http.Request) (*http.Response, error) {
|
|
return c.do(req)
|
|
}
|
|
|
|
func (c *client) do(req *http.Request) (*http.Response, error) {
|
|
for _, interceptor := range c.interceptors {
|
|
if err := interceptor.Intercept(req); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if c.authorizer != nil {
|
|
if err := c.authorizer.Modify(req); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
message := fmt.Sprintf("http status code: %d, body: %s", resp.StatusCode, string(body))
|
|
code := errors.GeneralCode
|
|
switch resp.StatusCode {
|
|
case http.StatusUnauthorized:
|
|
code = errors.UnAuthorizedCode
|
|
case http.StatusForbidden:
|
|
code = errors.ForbiddenCode
|
|
case http.StatusNotFound:
|
|
code = errors.NotFoundCode
|
|
case http.StatusTooManyRequests:
|
|
code = errors.RateLimitCode
|
|
}
|
|
return nil, errors.New(nil).WithCode(code).
|
|
WithMessage(message)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// parse the next page link from the link header
|
|
func next(link string) string {
|
|
links := lib.ParseLinks(link)
|
|
for _, lk := range links {
|
|
if lk.Rel == "next" {
|
|
return lk.URL
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func buildPingURL(endpoint string) string {
|
|
return fmt.Sprintf("%s/v2/", endpoint)
|
|
}
|
|
|
|
func buildCatalogURL(endpoint string) string {
|
|
return fmt.Sprintf("%s/v2/_catalog?n=1000", endpoint)
|
|
}
|
|
|
|
func buildTagListURL(endpoint, repository string) string {
|
|
return fmt.Sprintf("%s/v2/%s/tags/list", endpoint, repository)
|
|
}
|
|
|
|
func buildManifestURL(endpoint, repository, reference string) string {
|
|
return fmt.Sprintf("%s/v2/%s/manifests/%s", endpoint, repository, reference)
|
|
}
|
|
|
|
func buildBlobURL(endpoint, repository, reference string) string {
|
|
return fmt.Sprintf("%s/v2/%s/blobs/%s", endpoint, repository, reference)
|
|
}
|
|
|
|
func buildMountBlobURL(endpoint, repository, digest, from string) string {
|
|
return fmt.Sprintf("%s/v2/%s/blobs/uploads/?mount=%s&from=%s", endpoint, repository, digest, from)
|
|
}
|
|
|
|
func buildInitiateBlobUploadURL(endpoint, repository string) string {
|
|
return fmt.Sprintf("%s/v2/%s/blobs/uploads/", endpoint, repository)
|
|
}
|
|
|
|
func buildChunkBlobUploadURL(endpoint, location, digest string, lastChunk bool) (string, error) {
|
|
url, err := url.Parse(location)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
q := url.Query()
|
|
if lastChunk {
|
|
q.Set("digest", digest)
|
|
}
|
|
url.RawQuery = q.Encode()
|
|
if url.IsAbs() {
|
|
return url.String(), nil
|
|
}
|
|
// the "relativeurls" is enabled in registry
|
|
return endpoint + url.String(), nil
|
|
}
|
|
|
|
func buildMonolithicBlobUploadURL(endpoint, location, digest string) (string, error) {
|
|
url, err := url.Parse(location)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
q := url.Query()
|
|
q.Set("digest", digest)
|
|
url.RawQuery = q.Encode()
|
|
if url.IsAbs() {
|
|
return url.String(), nil
|
|
}
|
|
// the "relativeurls" is enabled in registry
|
|
return endpoint + url.String(), nil
|
|
}
|