Update resolver to handle endpoint configuration
Adds support for registry mirrors Adds support for multiple pull endpoints Adds capabilities to limit trust in public mirrors Fixes user agent header missing Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
parent
a0696b2bc6
commit
0b29c9c371
@ -31,7 +31,6 @@ import (
|
|||||||
|
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
"github.com/containerd/containerd/log"
|
"github.com/containerd/containerd/log"
|
||||||
"github.com/containerd/containerd/version"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/context/ctxhttp"
|
"golang.org/x/net/context/ctxhttp"
|
||||||
@ -41,7 +40,7 @@ type dockerAuthorizer struct {
|
|||||||
credentials func(string) (string, string, error)
|
credentials func(string) (string, string, error)
|
||||||
|
|
||||||
client *http.Client
|
client *http.Client
|
||||||
ua string
|
header http.Header
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
// indexed by host name
|
// indexed by host name
|
||||||
@ -50,15 +49,58 @@ type dockerAuthorizer struct {
|
|||||||
|
|
||||||
// NewAuthorizer creates a Docker authorizer using the provided function to
|
// NewAuthorizer creates a Docker authorizer using the provided function to
|
||||||
// get credentials for the token server or basic auth.
|
// get credentials for the token server or basic auth.
|
||||||
|
// Deprecated: Use NewDockerAuthorizer
|
||||||
func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
|
func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
|
||||||
if client == nil {
|
return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f))
|
||||||
client = http.DefaultClient
|
}
|
||||||
|
|
||||||
|
type authorizerConfig struct {
|
||||||
|
credentials func(string) (string, string, error)
|
||||||
|
client *http.Client
|
||||||
|
header http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizerOpt configures an authorizer
|
||||||
|
type AuthorizerOpt func(*authorizerConfig)
|
||||||
|
|
||||||
|
// WithAuthClient provides the HTTP client for the authorizer
|
||||||
|
func WithAuthClient(client *http.Client) AuthorizerOpt {
|
||||||
|
return func(opt *authorizerConfig) {
|
||||||
|
opt.client = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthCreds provides a credential function to the authorizer
|
||||||
|
func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt {
|
||||||
|
return func(opt *authorizerConfig) {
|
||||||
|
opt.credentials = creds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthHeader provides HTTP headers for authorization
|
||||||
|
func WithAuthHeader(hdr http.Header) AuthorizerOpt {
|
||||||
|
return func(opt *authorizerConfig) {
|
||||||
|
opt.header = hdr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDockerAuthorizer creates an authorizer using Docker's registry
|
||||||
|
// authentication spec.
|
||||||
|
// See https://docs.docker.com/registry/spec/auth/
|
||||||
|
func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer {
|
||||||
|
var ao authorizerConfig
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&ao)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ao.client == nil {
|
||||||
|
ao.client = http.DefaultClient
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dockerAuthorizer{
|
return &dockerAuthorizer{
|
||||||
credentials: f,
|
credentials: ao.credentials,
|
||||||
client: client,
|
client: ao.client,
|
||||||
ua: "containerd/" + version.Version,
|
header: ao.header,
|
||||||
handlers: make(map[string]*authHandler),
|
handlers: make(map[string]*authHandler),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,7 +157,7 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.handlers[host] = newAuthHandler(a.client, a.ua, c.scheme, common)
|
a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
|
||||||
return nil
|
return nil
|
||||||
} else if c.scheme == basicAuth && a.credentials != nil {
|
} else if c.scheme == basicAuth && a.credentials != nil {
|
||||||
username, secret, err := a.credentials(host)
|
username, secret, err := a.credentials(host)
|
||||||
@ -129,7 +171,7 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
|
|||||||
secret: secret,
|
secret: secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
a.handlers[host] = newAuthHandler(a.client, a.ua, c.scheme, common)
|
a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,7 +221,7 @@ type authResult struct {
|
|||||||
type authHandler struct {
|
type authHandler struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
ua string
|
header http.Header
|
||||||
|
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
|
||||||
@ -194,13 +236,9 @@ type authHandler struct {
|
|||||||
scopedTokens map[string]*authResult
|
scopedTokens map[string]*authResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuthHandler(client *http.Client, ua string, scheme authenticationScheme, opts tokenOptions) *authHandler {
|
func newAuthHandler(client *http.Client, hdr http.Header, scheme authenticationScheme, opts tokenOptions) *authHandler {
|
||||||
if client == nil {
|
|
||||||
client = http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
return &authHandler{
|
return &authHandler{
|
||||||
ua: ua,
|
header: hdr,
|
||||||
client: client,
|
client: client,
|
||||||
scheme: scheme,
|
scheme: scheme,
|
||||||
common: opts,
|
common: opts,
|
||||||
@ -313,8 +351,10 @@ func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions)
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
||||||
if ah.ua != "" {
|
if ah.header != nil {
|
||||||
req.Header.Set("User-Agent", ah.ua)
|
for k, v := range ah.header {
|
||||||
|
req.Header[k] = append(req.Header[k], v...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := ctxhttp.Do(ctx, ah.client, req)
|
resp, err := ctxhttp.Do(ctx, ah.client, req)
|
||||||
@ -363,8 +403,10 @@ func (ah *authHandler) fetchToken(ctx context.Context, to tokenOptions) (string,
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ah.ua != "" {
|
if ah.header != nil {
|
||||||
req.Header.Set("User-Agent", ah.ua)
|
for k, v := range ah.header {
|
||||||
|
req.Header[k] = append(req.Header[k], v...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reqParams := req.URL.Query()
|
reqParams := req.URL.Query()
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
@ -32,7 +32,6 @@ import (
|
|||||||
"github.com/docker/distribution/registry/api/errcode"
|
"github.com/docker/distribution/registry/api/errcode"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type dockerFetcher struct {
|
type dockerFetcher struct {
|
||||||
@ -40,26 +39,46 @@ type dockerFetcher struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
|
func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
|
||||||
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(
|
ctx = log.WithLogger(ctx, log.G(ctx).WithField("digest", desc.Digest))
|
||||||
logrus.Fields{
|
|
||||||
"base": r.base.String(),
|
|
||||||
"digest": desc.Digest,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
urls, err := r.getV2URLPaths(ctx, desc)
|
hosts := r.filterHosts(HostCapabilityPull)
|
||||||
if err != nil {
|
if len(hosts) == 0 {
|
||||||
return nil, err
|
return nil, errors.Wrap(errdefs.ErrNotFound, "no pull hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, err = contextWithRepositoryScope(ctx, r.refspec, false)
|
ctx, err := contextWithRepositoryScope(ctx, r.refspec, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newHTTPReadSeeker(desc.Size, func(offset int64) (io.ReadCloser, error) {
|
return newHTTPReadSeeker(desc.Size, func(offset int64) (io.ReadCloser, error) {
|
||||||
for _, u := range urls {
|
// firstly try fetch via external urls
|
||||||
rc, err := r.open(ctx, u, desc.MediaType, offset)
|
for _, us := range desc.URLs {
|
||||||
|
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", us))
|
||||||
|
|
||||||
|
u, err := url.Parse(us)
|
||||||
|
if err != nil {
|
||||||
|
log.G(ctx).WithError(err).Debug("failed to parse")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.G(ctx).Debug("trying alternative url")
|
||||||
|
|
||||||
|
// Try this first, parse it
|
||||||
|
host := RegistryHost{
|
||||||
|
Client: http.DefaultClient,
|
||||||
|
Host: u.Host,
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Path: u.Path,
|
||||||
|
Capabilities: HostCapabilityPull,
|
||||||
|
}
|
||||||
|
req := r.request(host, http.MethodGet)
|
||||||
|
// Strip namespace from base
|
||||||
|
req.path = u.Path
|
||||||
|
if u.RawQuery != "" {
|
||||||
|
req.path = req.path + "?" + u.RawQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := r.open(ctx, req, desc.MediaType, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errdefs.IsNotFound(err) {
|
if errdefs.IsNotFound(err) {
|
||||||
continue // try one of the other urls.
|
continue // try one of the other urls.
|
||||||
@ -71,6 +90,44 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
|
|||||||
return rc, nil
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try manifests endpoints for manifests types
|
||||||
|
switch desc.MediaType {
|
||||||
|
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
|
||||||
|
images.MediaTypeDockerSchema1Manifest,
|
||||||
|
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
|
||||||
|
|
||||||
|
for _, host := range r.hosts {
|
||||||
|
req := r.request(host, http.MethodGet, "manifests", desc.Digest.String())
|
||||||
|
|
||||||
|
rc, err := r.open(ctx, req, desc.MediaType, offset)
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
continue // try another host
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally use blobs endpoints
|
||||||
|
for _, host := range r.hosts {
|
||||||
|
req := r.request(host, http.MethodGet, "blobs", desc.Digest.String())
|
||||||
|
|
||||||
|
rc, err := r.open(ctx, req, desc.MediaType, offset)
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
continue // try another host
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil, errors.Wrapf(errdefs.ErrNotFound,
|
return nil, errors.Wrapf(errdefs.ErrNotFound,
|
||||||
"could not fetch content descriptor %v (%v) from remote",
|
"could not fetch content descriptor %v (%v) from remote",
|
||||||
desc.Digest, desc.MediaType)
|
desc.Digest, desc.MediaType)
|
||||||
@ -78,22 +135,17 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int64) (io.ReadCloser, error) {
|
func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string, offset int64) (io.ReadCloser, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
req.header.Set("Accept", strings.Join([]string{mediatype, `*`}, ", "))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Accept", strings.Join([]string{mediatype, `*`}, ", "))
|
|
||||||
|
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
// Note: "Accept-Ranges: bytes" cannot be trusted as some endpoints
|
// Note: "Accept-Ranges: bytes" cannot be trusted as some endpoints
|
||||||
// will return the header without supporting the range. The content
|
// will return the header without supporting the range. The content
|
||||||
// range must always be checked.
|
// range must always be checked.
|
||||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
|
req.header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.doRequestWithRetries(ctx, req, nil)
|
resp, err := req.doWithRetries(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -106,13 +158,13 @@ func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return nil, errors.Wrapf(errdefs.ErrNotFound, "content at %v not found", u)
|
return nil, errors.Wrapf(errdefs.ErrNotFound, "content at %v not found", req.String())
|
||||||
}
|
}
|
||||||
var registryErr errcode.Errors
|
var registryErr errcode.Errors
|
||||||
if err := json.NewDecoder(resp.Body).Decode(®istryErr); err != nil || registryErr.Len() < 1 {
|
if err := json.NewDecoder(resp.Body).Decode(®istryErr); err != nil || registryErr.Len() < 1 {
|
||||||
return nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
|
return nil, errors.Errorf("unexpected status code %v: %v", req.String(), resp.Status)
|
||||||
}
|
}
|
||||||
return nil, errors.Errorf("unexpected status code %v: %s - Server message: %s", u, resp.Status, registryErr.Error())
|
return nil, errors.Errorf("unexpected status code %v: %s - Server message: %s", req.String(), resp.Status, registryErr.Error())
|
||||||
}
|
}
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
cr := resp.Header.Get("content-range")
|
cr := resp.Header.Get("content-range")
|
||||||
@ -141,30 +193,3 @@ func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int
|
|||||||
|
|
||||||
return resp.Body, nil
|
return resp.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getV2URLPaths generates the candidate urls paths for the object based on the
|
|
||||||
// set of hints and the provided object id. URLs are returned in the order of
|
|
||||||
// most to least likely succeed.
|
|
||||||
func (r *dockerFetcher) getV2URLPaths(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
|
|
||||||
var urls []string
|
|
||||||
|
|
||||||
if len(desc.URLs) > 0 {
|
|
||||||
// handle fetch via external urls.
|
|
||||||
for _, u := range desc.URLs {
|
|
||||||
log.G(ctx).WithField("url", u).Debug("adding alternative url")
|
|
||||||
urls = append(urls, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch desc.MediaType {
|
|
||||||
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
|
|
||||||
images.MediaTypeDockerSchema1Manifest,
|
|
||||||
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
|
|
||||||
urls = append(urls, r.url(path.Join("manifests", desc.Digest.String())))
|
|
||||||
}
|
|
||||||
|
|
||||||
// always fallback to attempting to get the object out of the blobs store.
|
|
||||||
urls = append(urls, r.url(path.Join("blobs", desc.Digest.String())))
|
|
||||||
|
|
||||||
return urls, nil
|
|
||||||
}
|
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/distribution/registry/api/errcode"
|
"github.com/docker/distribution/registry/api/errcode"
|
||||||
@ -46,14 +47,30 @@ func TestFetcherOpen(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
|
u, err := url.Parse(s.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
f := dockerFetcher{&dockerBase{
|
f := dockerFetcher{&dockerBase{
|
||||||
client: s.Client(),
|
namespace: "nonempty",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
host := RegistryHost{
|
||||||
|
Client: s.Client(),
|
||||||
|
Host: u.Host,
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Path: u.Path,
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
req := f.request(host, http.MethodGet)
|
||||||
|
|
||||||
checkReader := func(o int64) {
|
checkReader := func(o int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
rc, err := f.open(ctx, s.URL, "", o)
|
|
||||||
|
rc, err := f.open(ctx, req, "", o)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to open: %+v", err)
|
t.Fatalf("failed to open: %+v", err)
|
||||||
}
|
}
|
||||||
@ -93,7 +110,7 @@ func TestFetcherOpen(t *testing.T) {
|
|||||||
// Check that server returning a different content range
|
// Check that server returning a different content range
|
||||||
// then requested errors
|
// then requested errors
|
||||||
start = 30
|
start = 30
|
||||||
_, err := f.open(ctx, s.URL, "", 20)
|
_, err = f.open(ctx, req, "", 20)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error opening with invalid server response")
|
t.Fatal("expected error opening with invalid server response")
|
||||||
}
|
}
|
||||||
@ -160,20 +177,34 @@ func TestDockerFetcherOpen(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
r := dockerFetcher{&dockerBase{
|
u, err := url.Parse(s.URL)
|
||||||
client: s.Client(),
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := dockerFetcher{&dockerBase{
|
||||||
|
namespace: "ns",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
got, err := r.open(context.TODO(), s.URL, "", 0)
|
host := RegistryHost{
|
||||||
|
Client: s.Client(),
|
||||||
|
Host: u.Host,
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Path: u.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := f.request(host, http.MethodGet)
|
||||||
|
|
||||||
|
got, err := f.open(context.TODO(), req, "", 0)
|
||||||
assert.Equal(t, tt.wantErr, (err != nil))
|
assert.Equal(t, tt.wantErr, (err != nil))
|
||||||
assert.Equal(t, tt.want, got)
|
assert.Equal(t, tt.want, got)
|
||||||
assert.Equal(t, tt.retries, 0)
|
assert.Equal(t, tt.retries, 0)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
var expectedError error
|
var expectedError error
|
||||||
if tt.wantServerMessageError {
|
if tt.wantServerMessageError {
|
||||||
expectedError = errors.Errorf("unexpected status code %v: %v %s - Server message: %s", s.URL, tt.mockedStatus, http.StatusText(tt.mockedStatus), tt.mockedErr.Error())
|
expectedError = errors.Errorf("unexpected status code %v/ns: %v %s - Server message: %s", s.URL, tt.mockedStatus, http.StatusText(tt.mockedStatus), tt.mockedErr.Error())
|
||||||
} else if tt.wantPlainError {
|
} else if tt.wantPlainError {
|
||||||
expectedError = errors.Errorf("unexpected status code %v: %v %s", s.URL, tt.mockedStatus, http.StatusText(tt.mockedStatus))
|
expectedError = errors.Errorf("unexpected status code %v/ns: %v %s", s.URL, tt.mockedStatus, http.StatusText(tt.mockedStatus))
|
||||||
}
|
}
|
||||||
assert.Equal(t, expectedError.Error(), err.Error())
|
assert.Equal(t, expectedError.Error(), err.Error())
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -59,9 +59,15 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
return nil, errors.Wrap(err, "failed to get status")
|
return nil, errors.Wrap(err, "failed to get status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hosts := p.filterHosts(HostCapabilityPush)
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
return nil, errors.Wrap(errdefs.ErrNotFound, "no push hosts")
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
isManifest bool
|
isManifest bool
|
||||||
existCheck string
|
existCheck []string
|
||||||
|
host = hosts[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
switch desc.MediaType {
|
switch desc.MediaType {
|
||||||
@ -69,21 +75,20 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
|
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
|
||||||
isManifest = true
|
isManifest = true
|
||||||
if p.tag == "" {
|
if p.tag == "" {
|
||||||
existCheck = path.Join("manifests", desc.Digest.String())
|
existCheck = []string{"manifests", desc.Digest.String()}
|
||||||
} else {
|
} else {
|
||||||
existCheck = path.Join("manifests", p.tag)
|
existCheck = []string{"manifests", p.tag}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
existCheck = path.Join("blobs", desc.Digest.String())
|
existCheck = []string{"blobs", desc.Digest.String()}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodHead, p.url(existCheck), nil)
|
req := p.request(host, http.MethodHead, existCheck...)
|
||||||
if err != nil {
|
req.header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
|
log.G(ctx).WithField("url", req.String()).Debugf("checking and pushing to")
|
||||||
resp, err := p.doRequestWithRetries(ctx, req, nil)
|
|
||||||
|
resp, err := req.doWithRetries(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Cause(err) != ErrInvalidAuthorization {
|
if errors.Cause(err) != ErrInvalidAuthorization {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -117,28 +122,22 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isManifest {
|
if isManifest {
|
||||||
var putPath string
|
var putPath []string
|
||||||
if p.tag != "" {
|
if p.tag != "" {
|
||||||
putPath = path.Join("manifests", p.tag)
|
putPath = []string{"manifests", p.tag}
|
||||||
} else {
|
} else {
|
||||||
putPath = path.Join("manifests", desc.Digest.String())
|
putPath = []string{"manifests", desc.Digest.String()}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err = http.NewRequest(http.MethodPut, p.url(putPath), nil)
|
req = p.request(host, http.MethodPut, putPath...)
|
||||||
if err != nil {
|
req.header.Add("Content-Type", desc.MediaType)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", desc.MediaType)
|
|
||||||
} else {
|
} else {
|
||||||
// Start upload request
|
// Start upload request
|
||||||
req, err = http.NewRequest(http.MethodPost, p.url("blobs", "uploads")+"/", nil)
|
req = p.request(host, http.MethodPost, "blobs", "uploads/")
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
if fromRepo := selectRepositoryMountCandidate(p.refspec, desc.Annotations); fromRepo != "" {
|
if fromRepo := selectRepositoryMountCandidate(p.refspec, desc.Annotations); fromRepo != "" {
|
||||||
req = requestWithMountFrom(req, desc.Digest.String(), fromRepo)
|
preq := requestWithMountFrom(req, desc.Digest.String(), fromRepo)
|
||||||
pctx := contextWithAppendPullRepositoryScope(ctx, fromRepo)
|
pctx := contextWithAppendPullRepositoryScope(ctx, fromRepo)
|
||||||
|
|
||||||
// NOTE: the fromRepo might be private repo and
|
// NOTE: the fromRepo might be private repo and
|
||||||
@ -147,7 +146,8 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
//
|
//
|
||||||
// for the private repo, we should remove mount-from
|
// for the private repo, we should remove mount-from
|
||||||
// query and send the request again.
|
// query and send the request again.
|
||||||
resp, err = p.doRequest(pctx, req)
|
resp, err = preq.do(pctx)
|
||||||
|
//resp, err = p.doRequest(pctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -157,16 +157,11 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
|
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
resp = nil
|
resp = nil
|
||||||
|
|
||||||
req, err = removeMountFromQuery(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
resp, err = p.doRequestWithRetries(ctx, req, nil)
|
resp, err = req.doWithRetries(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -186,31 +181,41 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
return nil, errors.Errorf("unexpected response: %s", resp.Status)
|
return nil, errors.Errorf("unexpected response: %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
location := resp.Header.Get("Location")
|
var (
|
||||||
|
location = resp.Header.Get("Location")
|
||||||
|
lurl *url.URL
|
||||||
|
lhost = host
|
||||||
|
)
|
||||||
// Support paths without host in location
|
// Support paths without host in location
|
||||||
if strings.HasPrefix(location, "/") {
|
if strings.HasPrefix(location, "/") {
|
||||||
// Support location string containing path and query
|
lurl, err = url.Parse(lhost.Scheme + "://" + lhost.Host + location)
|
||||||
qmIndex := strings.Index(location, "?")
|
|
||||||
if qmIndex > 0 {
|
|
||||||
u := p.base
|
|
||||||
u.Path = location[:qmIndex]
|
|
||||||
u.RawQuery = location[qmIndex+1:]
|
|
||||||
location = u.String()
|
|
||||||
} else {
|
|
||||||
u := p.base
|
|
||||||
u.Path = location
|
|
||||||
location = u.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = http.NewRequest(http.MethodPut, location, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrapf(err, "unable to parse location %v", location)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(location, "://") {
|
||||||
|
location = lhost.Scheme + "://" + location
|
||||||
|
}
|
||||||
|
lurl, err = url.Parse(location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "unable to parse location %v", location)
|
||||||
}
|
}
|
||||||
q := req.URL.Query()
|
|
||||||
q.Add("digest", desc.Digest.String())
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
|
if lurl.Host != lhost.Host || lhost.Scheme != lurl.Scheme {
|
||||||
|
|
||||||
|
lhost.Scheme = lurl.Scheme
|
||||||
|
lhost.Host = lurl.Host
|
||||||
|
log.G(ctx).WithField("host", lhost.Host).WithField("scheme", lhost.Scheme).Debug("upload changed destination")
|
||||||
|
|
||||||
|
// Strip authorizer if change to host or scheme
|
||||||
|
lhost.Authorizer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q := lurl.Query()
|
||||||
|
q.Add("digest", desc.Digest.String())
|
||||||
|
|
||||||
|
req = p.request(lhost, http.MethodPut)
|
||||||
|
req.path = lurl.Path + "?" + q.Encode()
|
||||||
}
|
}
|
||||||
p.tracker.SetStatus(ref, Status{
|
p.tracker.SetStatus(ref, Status{
|
||||||
Status: content.Status{
|
Status: content.Status{
|
||||||
@ -226,12 +231,14 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
respC := make(chan *http.Response, 1)
|
respC := make(chan *http.Response, 1)
|
||||||
|
|
||||||
req.Body = ioutil.NopCloser(pr)
|
req.body = func() (io.ReadCloser, error) {
|
||||||
req.ContentLength = desc.Size
|
return ioutil.NopCloser(pr), nil
|
||||||
|
}
|
||||||
|
req.size = desc.Size
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(respC)
|
defer close(respC)
|
||||||
resp, err = p.doRequest(ctx, req)
|
resp, err = req.do(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pr.CloseWithError(err)
|
pr.CloseWithError(err)
|
||||||
return
|
return
|
||||||
@ -355,24 +362,15 @@ func (pw *pushWriter) Truncate(size int64) error {
|
|||||||
return errors.New("cannot truncate remote upload")
|
return errors.New("cannot truncate remote upload")
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestWithMountFrom(req *http.Request, mount, from string) *http.Request {
|
func requestWithMountFrom(req *request, mount, from string) *request {
|
||||||
q := req.URL.Query()
|
creq := *req
|
||||||
|
|
||||||
q.Set("mount", mount)
|
sep := "?"
|
||||||
q.Set("from", from)
|
if strings.Contains(creq.path, sep) {
|
||||||
req.URL.RawQuery = q.Encode()
|
sep = "&"
|
||||||
return req
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeMountFromQuery(req *http.Request) (*http.Request, error) {
|
creq.path = creq.path + sep + "mount=" + mount + "&from=" + from
|
||||||
req, err := copyRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
q := req.URL.Query()
|
return &creq
|
||||||
q.Del("mount")
|
|
||||||
q.Del("from")
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
return req, nil
|
|
||||||
}
|
}
|
||||||
|
202
remotes/docker/registry.go
Normal file
202
remotes/docker/registry.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd 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 docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HostCapabilities represent the capabilities of the registry
|
||||||
|
// host. This also represents the set of operations for which
|
||||||
|
// the registry host may be trusted to perform.
|
||||||
|
//
|
||||||
|
// For example pushing is a capability which should only be
|
||||||
|
// performed on an upstream source, not a mirror.
|
||||||
|
// Resolving (the process of converting a name into a digest)
|
||||||
|
// must be considered a trusted operation and only done by
|
||||||
|
// a host which is trusted (or more preferably by secure process
|
||||||
|
// which can prove the provenance of the mapping). A public
|
||||||
|
// mirror should never be trusted to do a resolve action.
|
||||||
|
//
|
||||||
|
// | Registry Type | Pull | Resolve | Push |
|
||||||
|
// |------------------|------|---------|------|
|
||||||
|
// | Public Registry | yes | yes | yes |
|
||||||
|
// | Private Registry | yes | yes | yes |
|
||||||
|
// | Public Mirror | yes | no | no |
|
||||||
|
// | Private Mirror | yes | yes | no |
|
||||||
|
type HostCapabilities uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HostCapabilityPull represents the capability to fetch manifests
|
||||||
|
// and blobs by digest
|
||||||
|
HostCapabilityPull HostCapabilities = 1 << iota
|
||||||
|
|
||||||
|
// HostCapabilityResolve represents the capability to fetch manifests
|
||||||
|
// by name
|
||||||
|
HostCapabilityResolve
|
||||||
|
|
||||||
|
// HostCapabilityPush represents the capability to push blobs and
|
||||||
|
// manifests
|
||||||
|
HostCapabilityPush
|
||||||
|
|
||||||
|
// Reserved for future capabilities (i.e. search, catalog, remove)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c HostCapabilities) Has(t HostCapabilities) bool {
|
||||||
|
return c&t == t
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryHost represents a complete configuration for a registry
|
||||||
|
// host, representing the capabilities, authorizations, connection
|
||||||
|
// configuration, and location.
|
||||||
|
type RegistryHost struct {
|
||||||
|
Client *http.Client
|
||||||
|
Authorizer Authorizer
|
||||||
|
Host string
|
||||||
|
Scheme string
|
||||||
|
Path string
|
||||||
|
Capabilities HostCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryHosts fetches the registry hosts for a given namespace,
|
||||||
|
// provided by the host component of an distribution image reference.
|
||||||
|
type RegistryHosts func(string) ([]RegistryHost, error)
|
||||||
|
|
||||||
|
// Registries joins multiple registry configuration functions, using the same
|
||||||
|
// order as provided within the arguments. When an empty registry configuration
|
||||||
|
// is returned with a nil error, the next function will be called.
|
||||||
|
// NOTE: This function will not join configurations, as soon as a non-empty
|
||||||
|
// configuration is returned from a configuration function, it will be returned
|
||||||
|
// to the caller.
|
||||||
|
func Registries(registries ...RegistryHosts) RegistryHosts {
|
||||||
|
return func(host string) ([]RegistryHost, error) {
|
||||||
|
for _, registry := range registries {
|
||||||
|
config, err := registry(host)
|
||||||
|
if err != nil {
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
if len(config) > 0 {
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type registryOpts struct {
|
||||||
|
authorizer Authorizer
|
||||||
|
plainHTTP func(string) (bool, error)
|
||||||
|
host func(string) (string, error)
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryOpt defines a registry default option
|
||||||
|
type RegistryOpt func(*registryOpts)
|
||||||
|
|
||||||
|
// WithPlainHTTP configures registries to use plaintext http scheme
|
||||||
|
// for the provided host match function.
|
||||||
|
func WithPlainHTTP(f func(string) (bool, error)) RegistryOpt {
|
||||||
|
return func(opts *registryOpts) {
|
||||||
|
opts.plainHTTP = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthorizer configures the default authorizer for a registry
|
||||||
|
func WithAuthorizer(a Authorizer) RegistryOpt {
|
||||||
|
return func(opts *registryOpts) {
|
||||||
|
opts.authorizer = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHostTranslator defines the default translator to use for registry hosts
|
||||||
|
func WithHostTranslator(h func(string) (string, error)) RegistryOpt {
|
||||||
|
return func(opts *registryOpts) {
|
||||||
|
opts.host = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClient configures the default http client for a registry
|
||||||
|
func WithClient(c *http.Client) RegistryOpt {
|
||||||
|
return func(opts *registryOpts) {
|
||||||
|
opts.client = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureDefaultRegistries is used to create a default configuration for
|
||||||
|
// registries. For more advanced configurations or per-domain setups,
|
||||||
|
// the RegistryHosts interface should be used directly.
|
||||||
|
// NOTE: This function will always return a non-empty value or error
|
||||||
|
func ConfigureDefaultRegistries(ropts ...RegistryOpt) RegistryHosts {
|
||||||
|
var opts registryOpts
|
||||||
|
for _, opt := range ropts {
|
||||||
|
opt(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(host string) ([]RegistryHost, error) {
|
||||||
|
config := RegistryHost{
|
||||||
|
Client: opts.client,
|
||||||
|
Authorizer: opts.authorizer,
|
||||||
|
Host: host,
|
||||||
|
Scheme: "https",
|
||||||
|
Path: "/v2",
|
||||||
|
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Client == nil {
|
||||||
|
config.Client = http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.plainHTTP != nil {
|
||||||
|
match, err := opts.plainHTTP(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
config.Scheme = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.host != nil {
|
||||||
|
var err error
|
||||||
|
config.Host, err = opts.host(config.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if host == "docker.io" {
|
||||||
|
config.Host = "registry-1.docker.io"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []RegistryHost{config}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAllHosts is a host match function which is always true.
|
||||||
|
func MatchAllHosts(string) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchLocalhost is a host match function which returns true for
|
||||||
|
// localhost.
|
||||||
|
func MatchLocalhost(host string) (bool, error) {
|
||||||
|
for _, s := range []string{"localhost", "127.0.0.1", "[::1]"} {
|
||||||
|
if len(host) >= len(s) && host[0:len(s)] == s && (len(host) == len(s) || host[len(s)] == ':') {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host == "::1", nil
|
||||||
|
|
||||||
|
}
|
76
remotes/docker/registry_test.go
Normal file
76
remotes/docker/registry_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd 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 docker
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHasCapability(t *testing.T) {
|
||||||
|
var (
|
||||||
|
pull = HostCapabilityPull
|
||||||
|
rslv = HostCapabilityResolve
|
||||||
|
push = HostCapabilityPush
|
||||||
|
all = pull | rslv | push
|
||||||
|
)
|
||||||
|
for i, tc := range []struct {
|
||||||
|
c HostCapabilities
|
||||||
|
t HostCapabilities
|
||||||
|
e bool
|
||||||
|
}{
|
||||||
|
{all, pull, true},
|
||||||
|
{all, pull | rslv, true},
|
||||||
|
{all, pull | push, true},
|
||||||
|
{all, all, true},
|
||||||
|
{pull, all, false},
|
||||||
|
{pull, push, false},
|
||||||
|
{rslv, pull, false},
|
||||||
|
{pull | rslv, push, false},
|
||||||
|
{pull | rslv, rslv, true},
|
||||||
|
} {
|
||||||
|
if a := tc.c.Has(tc.t); a != tc.e {
|
||||||
|
t.Fatalf("%d: failed, expected %t, got %t", i, tc.e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchLocalhost(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
host string
|
||||||
|
match bool
|
||||||
|
}{
|
||||||
|
{"", false},
|
||||||
|
{"127.1.1.1", false},
|
||||||
|
{"127.0.0.1", true},
|
||||||
|
{"127.0.0.1:5000", true},
|
||||||
|
{"registry.org", false},
|
||||||
|
{"localhost", true},
|
||||||
|
{"localhost:5000", true},
|
||||||
|
{"[127:0:0:1]", false},
|
||||||
|
{"[::1]", true},
|
||||||
|
{"[::1]:5000", true},
|
||||||
|
{"::1", true},
|
||||||
|
} {
|
||||||
|
actual, _ := MatchLocalhost(tc.host)
|
||||||
|
if actual != tc.match {
|
||||||
|
if tc.match {
|
||||||
|
t.Logf("Expected match for %s", tc.host)
|
||||||
|
} else {
|
||||||
|
t.Logf("Unexpected match for %s", tc.host)
|
||||||
|
}
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,9 +18,10 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -46,6 +47,19 @@ var (
|
|||||||
// ErrInvalidAuthorization is used when credentials are passed to a server but
|
// ErrInvalidAuthorization is used when credentials are passed to a server but
|
||||||
// those credentials are rejected.
|
// those credentials are rejected.
|
||||||
ErrInvalidAuthorization = errors.New("authorization failed")
|
ErrInvalidAuthorization = errors.New("authorization failed")
|
||||||
|
|
||||||
|
// MaxManifestSize represents the largest size accepted from a registry
|
||||||
|
// during resolution. Larger manifests may be accepted using a
|
||||||
|
// resolution method other than the registry.
|
||||||
|
//
|
||||||
|
// NOTE: The max supported layers by some runtimes is 128 and individual
|
||||||
|
// layers will not contribute more than 256 bytes, making a
|
||||||
|
// reasonable limit for a large image manifests of 32K bytes.
|
||||||
|
// 4M bytes represents a much larger upper bound for images which may
|
||||||
|
// contain large annotations or be non-images. A proper manifest
|
||||||
|
// design puts large metadata in subobjects, as is consistent the
|
||||||
|
// intent of the manifest design.
|
||||||
|
MaxManifestSize int64 = 4 * 1048 * 1048
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authorizer is used to authorize HTTP requests based on 401 HTTP responses.
|
// Authorizer is used to authorize HTTP requests based on 401 HTTP responses.
|
||||||
@ -72,31 +86,38 @@ type Authorizer interface {
|
|||||||
|
|
||||||
// ResolverOptions are used to configured a new Docker register resolver
|
// ResolverOptions are used to configured a new Docker register resolver
|
||||||
type ResolverOptions struct {
|
type ResolverOptions struct {
|
||||||
// Authorizer is used to authorize registry requests
|
// Hosts returns registry host configurations for a namespace.
|
||||||
Authorizer Authorizer
|
Hosts RegistryHosts
|
||||||
|
|
||||||
// Credentials provides username and secret given a host.
|
|
||||||
// If username is empty but a secret is given, that secret
|
|
||||||
// is interpreted as a long lived token.
|
|
||||||
// Deprecated: use Authorizer
|
|
||||||
Credentials func(string) (string, string, error)
|
|
||||||
|
|
||||||
// Host provides the hostname given a namespace.
|
|
||||||
Host func(string) (string, error)
|
|
||||||
|
|
||||||
// Headers are the HTTP request header fields sent by the resolver
|
// Headers are the HTTP request header fields sent by the resolver
|
||||||
Headers http.Header
|
Headers http.Header
|
||||||
|
|
||||||
// PlainHTTP specifies to use plain http and not https
|
|
||||||
PlainHTTP bool
|
|
||||||
|
|
||||||
// Client is the http client to used when making registry requests
|
|
||||||
Client *http.Client
|
|
||||||
|
|
||||||
// Tracker is used to track uploads to the registry. This is used
|
// Tracker is used to track uploads to the registry. This is used
|
||||||
// since the registry does not have upload tracking and the existing
|
// since the registry does not have upload tracking and the existing
|
||||||
// mechanism for getting blob upload status is expensive.
|
// mechanism for getting blob upload status is expensive.
|
||||||
Tracker StatusTracker
|
Tracker StatusTracker
|
||||||
|
|
||||||
|
// Authorizer is used to authorize registry requests
|
||||||
|
// Deprecated: use Hosts
|
||||||
|
Authorizer Authorizer
|
||||||
|
|
||||||
|
// Credentials provides username and secret given a host.
|
||||||
|
// If username is empty but a secret is given, that secret
|
||||||
|
// is interpreted as a long lived token.
|
||||||
|
// Deprecated: use Hosts
|
||||||
|
Credentials func(string) (string, string, error)
|
||||||
|
|
||||||
|
// Host provides the hostname given a namespace.
|
||||||
|
// Deprecated: use Hosts
|
||||||
|
Host func(string) (string, error)
|
||||||
|
|
||||||
|
// PlainHTTP specifies to use plain http and not https
|
||||||
|
// Deprecated: use Hosts
|
||||||
|
PlainHTTP bool
|
||||||
|
|
||||||
|
// Client is the http client to used when making registry requests
|
||||||
|
// Deprecated: use Hosts
|
||||||
|
Client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultHost is the default host function.
|
// DefaultHost is the default host function.
|
||||||
@ -108,12 +129,9 @@ func DefaultHost(ns string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type dockerResolver struct {
|
type dockerResolver struct {
|
||||||
auth Authorizer
|
hosts RegistryHosts
|
||||||
host func(string) (string, error)
|
header http.Header
|
||||||
headers http.Header
|
resolveHeader http.Header
|
||||||
uagent string
|
|
||||||
plainHTTP bool
|
|
||||||
client *http.Client
|
|
||||||
tracker StatusTracker
|
tracker StatusTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,38 +140,55 @@ func NewResolver(options ResolverOptions) remotes.Resolver {
|
|||||||
if options.Tracker == nil {
|
if options.Tracker == nil {
|
||||||
options.Tracker = NewInMemoryTracker()
|
options.Tracker = NewInMemoryTracker()
|
||||||
}
|
}
|
||||||
if options.Host == nil {
|
|
||||||
options.Host = DefaultHost
|
|
||||||
}
|
|
||||||
if options.Headers == nil {
|
if options.Headers == nil {
|
||||||
options.Headers = make(http.Header)
|
options.Headers = make(http.Header)
|
||||||
}
|
}
|
||||||
|
if _, ok := options.Headers["User-Agent"]; !ok {
|
||||||
|
options.Headers.Set("User-Agent", "containerd/"+version.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveHeader := http.Header{}
|
||||||
if _, ok := options.Headers["Accept"]; !ok {
|
if _, ok := options.Headers["Accept"]; !ok {
|
||||||
// set headers for all the types we support for resolution.
|
// set headers for all the types we support for resolution.
|
||||||
options.Headers.Set("Accept", strings.Join([]string{
|
resolveHeader.Set("Accept", strings.Join([]string{
|
||||||
images.MediaTypeDockerSchema2Manifest,
|
images.MediaTypeDockerSchema2Manifest,
|
||||||
images.MediaTypeDockerSchema2ManifestList,
|
images.MediaTypeDockerSchema2ManifestList,
|
||||||
ocispec.MediaTypeImageManifest,
|
ocispec.MediaTypeImageManifest,
|
||||||
ocispec.MediaTypeImageIndex, "*"}, ", "))
|
ocispec.MediaTypeImageIndex, "*"}, ", "))
|
||||||
}
|
|
||||||
ua := options.Headers.Get("User-Agent")
|
|
||||||
if ua != "" {
|
|
||||||
options.Headers.Del("User-Agent")
|
|
||||||
} else {
|
} else {
|
||||||
ua = "containerd/" + version.Version
|
resolveHeader["Accept"] = options.Headers["Accept"]
|
||||||
|
delete(options.Headers, "Accept")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Hosts == nil {
|
||||||
|
opts := []RegistryOpt{}
|
||||||
|
if options.Host != nil {
|
||||||
|
opts = append(opts, WithHostTranslator(options.Host))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Authorizer == nil {
|
if options.Authorizer == nil {
|
||||||
options.Authorizer = NewAuthorizer(options.Client, options.Credentials)
|
options.Authorizer = NewDockerAuthorizer(
|
||||||
options.Authorizer.(*dockerAuthorizer).ua = ua
|
WithAuthClient(options.Client),
|
||||||
|
WithAuthHeader(options.Headers),
|
||||||
|
WithAuthCreds(options.Credentials))
|
||||||
|
}
|
||||||
|
opts = append(opts, WithAuthorizer(options.Authorizer))
|
||||||
|
|
||||||
|
if options.Client != nil {
|
||||||
|
opts = append(opts, WithClient(options.Client))
|
||||||
|
}
|
||||||
|
if options.PlainHTTP {
|
||||||
|
opts = append(opts, WithPlainHTTP(MatchAllHosts))
|
||||||
|
} else {
|
||||||
|
opts = append(opts, WithPlainHTTP(MatchLocalhost))
|
||||||
|
}
|
||||||
|
options.Hosts = ConfigureDefaultRegistries(opts...)
|
||||||
}
|
}
|
||||||
return &dockerResolver{
|
return &dockerResolver{
|
||||||
auth: options.Authorizer,
|
hosts: options.Hosts,
|
||||||
host: options.Host,
|
header: options.Headers,
|
||||||
headers: options.Headers,
|
resolveHeader: resolveHeader,
|
||||||
uagent: ua,
|
|
||||||
plainHTTP: options.PlainHTTP,
|
|
||||||
client: options.Client,
|
|
||||||
tracker: options.Tracker,
|
tracker: options.Tracker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,13 +236,11 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
|||||||
return "", ocispec.Descriptor{}, err
|
return "", ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fetcher := dockerFetcher{
|
|
||||||
dockerBase: base,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
urls []string
|
lastErr error
|
||||||
|
paths [][]string
|
||||||
dgst = refspec.Digest()
|
dgst = refspec.Digest()
|
||||||
|
caps = HostCapabilityPull
|
||||||
)
|
)
|
||||||
|
|
||||||
if dgst != "" {
|
if dgst != "" {
|
||||||
@ -218,28 +251,37 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// turns out, we have a valid digest, make a url.
|
// turns out, we have a valid digest, make a url.
|
||||||
urls = append(urls, fetcher.url("manifests", dgst.String()))
|
paths = append(paths, []string{"manifests", dgst.String()})
|
||||||
|
|
||||||
// fallback to blobs on not found.
|
// fallback to blobs on not found.
|
||||||
urls = append(urls, fetcher.url("blobs", dgst.String()))
|
paths = append(paths, []string{"blobs", dgst.String()})
|
||||||
} else {
|
} else {
|
||||||
urls = append(urls, fetcher.url("manifests", refspec.Object))
|
// Add
|
||||||
|
paths = append(paths, []string{"manifests", refspec.Object})
|
||||||
|
caps |= HostCapabilityResolve
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := base.filterHosts(caps)
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
return "", ocispec.Descriptor{}, errors.Wrap(errdefs.ErrNotFound, "no resolve hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, err = contextWithRepositoryScope(ctx, refspec, false)
|
ctx, err = contextWithRepositoryScope(ctx, refspec, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", ocispec.Descriptor{}, err
|
return "", ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
for _, u := range urls {
|
|
||||||
req, err := http.NewRequest(http.MethodHead, u, nil)
|
for _, u := range paths {
|
||||||
if err != nil {
|
for _, host := range hosts {
|
||||||
return "", ocispec.Descriptor{}, err
|
ctx := log.WithLogger(ctx, log.G(ctx).WithField("host", host.Host))
|
||||||
|
|
||||||
|
req := base.request(host, http.MethodHead, u...)
|
||||||
|
for key, value := range r.resolveHeader {
|
||||||
|
req.header[key] = append(req.header[key], value...)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header = r.headers
|
|
||||||
|
|
||||||
log.G(ctx).Debug("resolving")
|
log.G(ctx).Debug("resolving")
|
||||||
resp, err := fetcher.doRequestWithRetries(ctx, req, nil)
|
resp, err := req.doWithRetries(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Cause(err) == ErrInvalidAuthorization {
|
if errors.Cause(err) == ErrInvalidAuthorization {
|
||||||
err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization")
|
err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization")
|
||||||
@ -255,28 +297,33 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
|||||||
return "", ocispec.Descriptor{}, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
|
return "", ocispec.Descriptor{}, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
|
||||||
}
|
}
|
||||||
size := resp.ContentLength
|
size := resp.ContentLength
|
||||||
|
contentType := getManifestMediaType(resp)
|
||||||
|
|
||||||
|
// if no digest was provided, then only a resolve
|
||||||
|
// trusted registry was contacted, in this case use
|
||||||
|
// the digest header (or content from GET)
|
||||||
|
if dgst == "" {
|
||||||
// this is the only point at which we trust the registry. we use the
|
// this is the only point at which we trust the registry. we use the
|
||||||
// content headers to assemble a descriptor for the name. when this becomes
|
// content headers to assemble a descriptor for the name. when this becomes
|
||||||
// more robust, we mostly get this information from a secure trust store.
|
// more robust, we mostly get this information from a secure trust store.
|
||||||
dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
|
dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
|
||||||
contentType := getManifestMediaType(resp)
|
|
||||||
|
|
||||||
if dgstHeader != "" && size != -1 {
|
if dgstHeader != "" && size != -1 {
|
||||||
if err := dgstHeader.Validate(); err != nil {
|
if err := dgstHeader.Validate(); err != nil {
|
||||||
return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
|
return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
|
||||||
}
|
}
|
||||||
dgst = dgstHeader
|
dgst = dgstHeader
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
if dgst == "" || size == -1 {
|
||||||
log.G(ctx).Debug("no Docker-Content-Digest header, fetching manifest instead")
|
log.G(ctx).Debug("no Docker-Content-Digest header, fetching manifest instead")
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
req = base.request(host, http.MethodGet, u...)
|
||||||
if err != nil {
|
for key, value := range r.resolveHeader {
|
||||||
return "", ocispec.Descriptor{}, err
|
req.header[key] = append(req.header[key], value...)
|
||||||
}
|
}
|
||||||
req.Header = r.headers
|
|
||||||
|
|
||||||
resp, err := fetcher.doRequestWithRetries(ctx, req, nil)
|
resp, err := req.doWithRetries(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", ocispec.Descriptor{}, err
|
return "", ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
@ -285,6 +332,7 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
|||||||
bodyReader := countingReader{reader: resp.Body}
|
bodyReader := countingReader{reader: resp.Body}
|
||||||
|
|
||||||
contentType = getManifestMediaType(resp)
|
contentType = getManifestMediaType(resp)
|
||||||
|
if dgst == "" {
|
||||||
if contentType == images.MediaTypeDockerSchema1Manifest {
|
if contentType == images.MediaTypeDockerSchema1Manifest {
|
||||||
b, err := schema1.ReadStripSignature(&bodyReader)
|
b, err := schema1.ReadStripSignature(&bodyReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -298,8 +346,18 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
|||||||
return "", ocispec.Descriptor{}, err
|
return "", ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if _, err := io.Copy(ioutil.Discard, &bodyReader); err != nil {
|
||||||
|
return "", ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
size = bodyReader.bytesRead
|
size = bodyReader.bytesRead
|
||||||
}
|
}
|
||||||
|
// Prevent resolving to excessively large manifests
|
||||||
|
if size > MaxManifestSize {
|
||||||
|
if lastErr == nil {
|
||||||
|
lastErr = errors.Wrapf(errdefs.ErrNotFound, "rejecting %d byte manifest for %s", size, ref)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
desc := ocispec.Descriptor{
|
desc := ocispec.Descriptor{
|
||||||
Digest: dgst,
|
Digest: dgst,
|
||||||
@ -310,8 +368,13 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
|||||||
log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
|
log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
|
||||||
return ref, desc, nil
|
return ref, desc, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "", ocispec.Descriptor{}, errors.Errorf("%v not found", ref)
|
if lastErr == nil {
|
||||||
|
lastErr = errors.Wrap(errdefs.ErrNotFound, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ocispec.Descriptor{}, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
|
func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
|
||||||
@ -357,55 +420,57 @@ func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher
|
|||||||
|
|
||||||
type dockerBase struct {
|
type dockerBase struct {
|
||||||
refspec reference.Spec
|
refspec reference.Spec
|
||||||
base url.URL
|
namespace string
|
||||||
uagent string
|
hosts []RegistryHost
|
||||||
|
header http.Header
|
||||||
client *http.Client
|
|
||||||
auth Authorizer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
||||||
var (
|
|
||||||
err error
|
|
||||||
base url.URL
|
|
||||||
)
|
|
||||||
|
|
||||||
host := refspec.Hostname()
|
host := refspec.Hostname()
|
||||||
base.Host = host
|
hosts, err := r.hosts(host)
|
||||||
if r.host != nil {
|
|
||||||
base.Host, err = r.host(host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
base.Scheme = "https"
|
|
||||||
if r.plainHTTP || strings.HasPrefix(base.Host, "localhost:") {
|
|
||||||
base.Scheme = "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
|
|
||||||
base.Path = path.Join("/v2", prefix)
|
|
||||||
|
|
||||||
return &dockerBase{
|
return &dockerBase{
|
||||||
refspec: refspec,
|
refspec: refspec,
|
||||||
base: base,
|
namespace: strings.TrimPrefix(refspec.Locator, host+"/"),
|
||||||
uagent: r.uagent,
|
hosts: hosts,
|
||||||
client: r.client,
|
header: r.header,
|
||||||
auth: r.auth,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) url(ps ...string) string {
|
func (r *dockerBase) filterHosts(caps HostCapabilities) (hosts []RegistryHost) {
|
||||||
url := r.base
|
for _, host := range r.hosts {
|
||||||
url.Path = path.Join(url.Path, path.Join(ps...))
|
if host.Capabilities.Has(caps) {
|
||||||
return url.String()
|
hosts = append(hosts, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) authorize(ctx context.Context, req *http.Request) error {
|
func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *request {
|
||||||
|
header := http.Header{}
|
||||||
|
for key, value := range r.header {
|
||||||
|
header[key] = append(header[key], value...)
|
||||||
|
}
|
||||||
|
parts := append([]string{"/", host.Path, r.namespace}, ps...)
|
||||||
|
p := path.Join(parts...)
|
||||||
|
// Join strips trailing slash, re-add ending "/" if included
|
||||||
|
if len(parts) > 0 && strings.HasSuffix(parts[len(parts)-1], "/") {
|
||||||
|
p = p + "/"
|
||||||
|
}
|
||||||
|
return &request{
|
||||||
|
method: method,
|
||||||
|
path: p,
|
||||||
|
header: header,
|
||||||
|
host: host,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *request) authorize(ctx context.Context, req *http.Request) error {
|
||||||
// Check if has header for host
|
// Check if has header for host
|
||||||
if r.auth != nil {
|
if r.host.Authorizer != nil {
|
||||||
if err := r.auth.Authorize(ctx, req); err != nil {
|
if err := r.host.Authorizer.Authorize(ctx, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -413,83 +478,132 @@ func (r *dockerBase) authorize(ctx context.Context, req *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
type request struct {
|
||||||
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
|
method string
|
||||||
log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request")
|
path string
|
||||||
req.Header.Set("User-Agent", r.uagent)
|
header http.Header
|
||||||
|
host RegistryHost
|
||||||
|
body func() (io.ReadCloser, error)
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *request) do(ctx context.Context) (*http.Response, error) {
|
||||||
|
u := r.host.Scheme + "://" + r.host.Host + r.path
|
||||||
|
req, err := http.NewRequest(r.method, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header = r.header
|
||||||
|
if r.body != nil {
|
||||||
|
req.GetBody = r.body
|
||||||
|
if r.size > 0 {
|
||||||
|
req.ContentLength = r.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", u))
|
||||||
|
log.G(ctx).WithFields(requestFields(req)).Debug("do request")
|
||||||
if err := r.authorize(ctx, req); err != nil {
|
if err := r.authorize(ctx, req); err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to authorize")
|
return nil, errors.Wrap(err, "failed to authorize")
|
||||||
}
|
}
|
||||||
resp, err := ctxhttp.Do(ctx, r.client, req)
|
resp, err := ctxhttp.Do(ctx, r.host.Client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to do request")
|
return nil, errors.Wrap(err, "failed to do request")
|
||||||
}
|
}
|
||||||
log.G(ctx).WithFields(logrus.Fields{
|
log.G(ctx).WithFields(responseFields(resp)).Debug("fetch response received")
|
||||||
"status": resp.Status,
|
|
||||||
"response.headers": resp.Header,
|
|
||||||
}).Debug("fetch response received")
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) {
|
func (r *request) doWithRetries(ctx context.Context, responses []*http.Response) (*http.Response, error) {
|
||||||
resp, err := r.doRequest(ctx, req)
|
resp, err := r.do(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
responses = append(responses, resp)
|
responses = append(responses, resp)
|
||||||
req, err = r.retryRequest(ctx, req, responses)
|
retry, err := r.retryRequest(ctx, responses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if req != nil {
|
if retry {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return r.doRequestWithRetries(ctx, req, responses)
|
return r.doWithRetries(ctx, responses)
|
||||||
}
|
}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) {
|
func (r *request) retryRequest(ctx context.Context, responses []*http.Response) (bool, error) {
|
||||||
if len(responses) > 5 {
|
if len(responses) > 5 {
|
||||||
return nil, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
last := responses[len(responses)-1]
|
last := responses[len(responses)-1]
|
||||||
switch last.StatusCode {
|
switch last.StatusCode {
|
||||||
case http.StatusUnauthorized:
|
case http.StatusUnauthorized:
|
||||||
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
||||||
if r.auth != nil {
|
if r.host.Authorizer != nil {
|
||||||
if err := r.auth.AddResponses(ctx, responses); err == nil {
|
if err := r.host.Authorizer.AddResponses(ctx, responses); err == nil {
|
||||||
return copyRequest(req)
|
return true, nil
|
||||||
} else if !errdefs.IsNotImplemented(err) {
|
} else if !errdefs.IsNotImplemented(err) {
|
||||||
return nil, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
return false, nil
|
||||||
case http.StatusMethodNotAllowed:
|
case http.StatusMethodNotAllowed:
|
||||||
// Support registries which have not properly implemented the HEAD method for
|
// Support registries which have not properly implemented the HEAD method for
|
||||||
// manifests endpoint
|
// manifests endpoint
|
||||||
if req.Method == http.MethodHead && strings.Contains(req.URL.Path, "/manifests/") {
|
if r.method == http.MethodHead && strings.Contains(r.path, "/manifests/") {
|
||||||
// TODO: copy request?
|
r.method = http.MethodGet
|
||||||
req.Method = http.MethodGet
|
return true, nil
|
||||||
return copyRequest(req)
|
|
||||||
}
|
}
|
||||||
case http.StatusRequestTimeout, http.StatusTooManyRequests:
|
case http.StatusRequestTimeout, http.StatusTooManyRequests:
|
||||||
return copyRequest(req)
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle 50x errors accounting for attempt history
|
// TODO: Handle 50x errors accounting for attempt history
|
||||||
return nil, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyRequest(req *http.Request) (*http.Request, error) {
|
func (r *request) String() string {
|
||||||
ireq := *req
|
return r.host.Scheme + "://" + r.host.Host + r.path
|
||||||
if ireq.GetBody != nil {
|
}
|
||||||
var err error
|
|
||||||
ireq.Body, err = ireq.GetBody()
|
func requestFields(req *http.Request) logrus.Fields {
|
||||||
if err != nil {
|
fields := map[string]interface{}{
|
||||||
return nil, err
|
"request.method": req.Method,
|
||||||
|
}
|
||||||
|
for k, vals := range req.Header {
|
||||||
|
k = strings.ToLower(k)
|
||||||
|
if k == "authorization" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, v := range vals {
|
||||||
|
field := "request.header." + k
|
||||||
|
if i > 0 {
|
||||||
|
field = fmt.Sprintf("%s.%d", field, i)
|
||||||
|
}
|
||||||
|
fields[field] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &ireq, nil
|
|
||||||
|
return logrus.Fields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseFields(resp *http.Response) logrus.Fields {
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"response.status": resp.Status,
|
||||||
|
}
|
||||||
|
for k, vals := range resp.Header {
|
||||||
|
k = strings.ToLower(k)
|
||||||
|
for i, v := range vals {
|
||||||
|
field := "response.header." + k
|
||||||
|
if i > 0 {
|
||||||
|
field = fmt.Sprintf("%s.%d", field, i)
|
||||||
|
}
|
||||||
|
fields[field] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return logrus.Fields(fields)
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,7 @@ func TestHTTPResolver(t *testing.T) {
|
|||||||
s := func(h http.Handler) (string, ResolverOptions, func()) {
|
s := func(h http.Handler) (string, ResolverOptions, func()) {
|
||||||
s := httptest.NewServer(h)
|
s := httptest.NewServer(h)
|
||||||
|
|
||||||
options := ResolverOptions{
|
options := ResolverOptions{}
|
||||||
PlainHTTP: true,
|
|
||||||
}
|
|
||||||
base := s.URL[7:] // strip "http://"
|
base := s.URL[7:] // strip "http://"
|
||||||
return base, options, s.Close
|
return base, options, s.Close
|
||||||
}
|
}
|
||||||
@ -69,9 +67,12 @@ func TestBasicResolver(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
base, options, close := tlsServer(wrapped)
|
base, options, close := tlsServer(wrapped)
|
||||||
options.Authorizer = NewAuthorizer(options.Client, func(string) (string, string, error) {
|
options.Hosts = ConfigureDefaultRegistries(
|
||||||
|
WithClient(options.Client),
|
||||||
|
WithAuthorizer(NewAuthorizer(options.Client, func(string) (string, string, error) {
|
||||||
return "user1", "password1", nil
|
return "user1", "password1", nil
|
||||||
})
|
})),
|
||||||
|
)
|
||||||
return base, options, close
|
return base, options, close
|
||||||
}
|
}
|
||||||
runBasicTest(t, "testname", basicAuth)
|
runBasicTest(t, "testname", basicAuth)
|
||||||
@ -215,7 +216,13 @@ func withTokenServer(th http.Handler, creds func(string) (string, string, error)
|
|||||||
})
|
})
|
||||||
|
|
||||||
base, options, close := tlsServer(wrapped)
|
base, options, close := tlsServer(wrapped)
|
||||||
options.Authorizer = NewAuthorizer(options.Client, creds)
|
options.Hosts = ConfigureDefaultRegistries(
|
||||||
|
WithClient(options.Client),
|
||||||
|
WithAuthorizer(NewDockerAuthorizer(
|
||||||
|
WithAuthClient(options.Client),
|
||||||
|
WithAuthCreds(creds),
|
||||||
|
)),
|
||||||
|
)
|
||||||
options.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert)
|
options.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert)
|
||||||
return base, options, func() {
|
return base, options, func() {
|
||||||
s.Close()
|
s.Close()
|
||||||
@ -232,14 +239,17 @@ func tlsServer(h http.Handler) (string, ResolverOptions, func()) {
|
|||||||
cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0])
|
cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0])
|
||||||
capool.AddCert(cert)
|
capool.AddCert(cert)
|
||||||
|
|
||||||
options := ResolverOptions{
|
client := &http.Client{
|
||||||
Client: &http.Client{
|
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
RootCAs: capool,
|
RootCAs: capool,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
options := ResolverOptions{
|
||||||
|
Hosts: ConfigureDefaultRegistries(WithClient(client)),
|
||||||
|
// Set deprecated field for tests to use for configuration
|
||||||
|
Client: client,
|
||||||
}
|
}
|
||||||
base := s.URL[8:] // strip "https://"
|
base := s.URL[8:] // strip "https://"
|
||||||
return base, options, s.Close
|
return base, options, s.Close
|
||||||
|
Loading…
Reference in New Issue
Block a user