Skip to content

Commit 1ff1f87

Browse files
Merge pull request #3400 from dmcgowan/registry-configuration
Update resolver to handle endpoint configuration
2 parents 569f500 + 0b29c9c commit 1ff1f87

8 files changed

Lines changed: 840 additions & 342 deletions

File tree

remotes/docker/authorizer.go

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131

3232
"github.com/containerd/containerd/errdefs"
3333
"github.com/containerd/containerd/log"
34-
"github.com/containerd/containerd/version"
3534
"github.com/pkg/errors"
3635
"github.com/sirupsen/logrus"
3736
"golang.org/x/net/context/ctxhttp"
@@ -41,7 +40,7 @@ type dockerAuthorizer struct {
4140
credentials func(string) (string, string, error)
4241

4342
client *http.Client
44-
ua string
43+
header http.Header
4544
mu sync.Mutex
4645

4746
// indexed by host name
@@ -50,15 +49,58 @@ type dockerAuthorizer struct {
5049

5150
// NewAuthorizer creates a Docker authorizer using the provided function to
5251
// get credentials for the token server or basic auth.
52+
// Deprecated: Use NewDockerAuthorizer
5353
func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
54-
if client == nil {
55-
client = http.DefaultClient
54+
return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f))
55+
}
56+
57+
type authorizerConfig struct {
58+
credentials func(string) (string, string, error)
59+
client *http.Client
60+
header http.Header
61+
}
62+
63+
// AuthorizerOpt configures an authorizer
64+
type AuthorizerOpt func(*authorizerConfig)
65+
66+
// WithAuthClient provides the HTTP client for the authorizer
67+
func WithAuthClient(client *http.Client) AuthorizerOpt {
68+
return func(opt *authorizerConfig) {
69+
opt.client = client
70+
}
71+
}
72+
73+
// WithAuthCreds provides a credential function to the authorizer
74+
func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt {
75+
return func(opt *authorizerConfig) {
76+
opt.credentials = creds
77+
}
78+
}
79+
80+
// WithAuthHeader provides HTTP headers for authorization
81+
func WithAuthHeader(hdr http.Header) AuthorizerOpt {
82+
return func(opt *authorizerConfig) {
83+
opt.header = hdr
84+
}
85+
}
86+
87+
// NewDockerAuthorizer creates an authorizer using Docker's registry
88+
// authentication spec.
89+
// See https://docs.docker.com/registry/spec/auth/
90+
func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer {
91+
var ao authorizerConfig
92+
for _, opt := range opts {
93+
opt(&ao)
94+
}
95+
96+
if ao.client == nil {
97+
ao.client = http.DefaultClient
5698
}
5799

58100
return &dockerAuthorizer{
59-
credentials: f,
60-
client: client,
61-
ua: "containerd/" + version.Version,
101+
credentials: ao.credentials,
102+
client: ao.client,
103+
header: ao.header,
62104
handlers: make(map[string]*authHandler),
63105
}
64106
}
@@ -115,7 +157,7 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
115157
return err
116158
}
117159

118-
a.handlers[host] = newAuthHandler(a.client, a.ua, c.scheme, common)
160+
a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
119161
return nil
120162
} else if c.scheme == basicAuth && a.credentials != nil {
121163
username, secret, err := a.credentials(host)
@@ -129,7 +171,7 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
129171
secret: secret,
130172
}
131173

132-
a.handlers[host] = newAuthHandler(a.client, a.ua, c.scheme, common)
174+
a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
133175
return nil
134176
}
135177
}
@@ -179,7 +221,7 @@ type authResult struct {
179221
type authHandler struct {
180222
sync.Mutex
181223

182-
ua string
224+
header http.Header
183225

184226
client *http.Client
185227

@@ -194,13 +236,9 @@ type authHandler struct {
194236
scopedTokens map[string]*authResult
195237
}
196238

197-
func newAuthHandler(client *http.Client, ua string, scheme authenticationScheme, opts tokenOptions) *authHandler {
198-
if client == nil {
199-
client = http.DefaultClient
200-
}
201-
239+
func newAuthHandler(client *http.Client, hdr http.Header, scheme authenticationScheme, opts tokenOptions) *authHandler {
202240
return &authHandler{
203-
ua: ua,
241+
header: hdr,
204242
client: client,
205243
scheme: scheme,
206244
common: opts,
@@ -313,8 +351,10 @@ func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions)
313351
return "", err
314352
}
315353
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
316-
if ah.ua != "" {
317-
req.Header.Set("User-Agent", ah.ua)
354+
if ah.header != nil {
355+
for k, v := range ah.header {
356+
req.Header[k] = append(req.Header[k], v...)
357+
}
318358
}
319359

320360
resp, err := ctxhttp.Do(ctx, ah.client, req)
@@ -363,8 +403,10 @@ func (ah *authHandler) fetchToken(ctx context.Context, to tokenOptions) (string,
363403
return "", err
364404
}
365405

366-
if ah.ua != "" {
367-
req.Header.Set("User-Agent", ah.ua)
406+
if ah.header != nil {
407+
for k, v := range ah.header {
408+
req.Header[k] = append(req.Header[k], v...)
409+
}
368410
}
369411

370412
reqParams := req.URL.Query()

remotes/docker/fetcher.go

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"io"
2424
"io/ioutil"
2525
"net/http"
26-
"path"
26+
"net/url"
2727
"strings"
2828

2929
"github.com/containerd/containerd/errdefs"
@@ -32,34 +32,53 @@ import (
3232
"github.com/docker/distribution/registry/api/errcode"
3333
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3434
"github.com/pkg/errors"
35-
"github.com/sirupsen/logrus"
3635
)
3736

3837
type dockerFetcher struct {
3938
*dockerBase
4039
}
4140

4241
func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
43-
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(
44-
logrus.Fields{
45-
"base": r.base.String(),
46-
"digest": desc.Digest,
47-
},
48-
))
49-
50-
urls, err := r.getV2URLPaths(ctx, desc)
51-
if err != nil {
52-
return nil, err
42+
ctx = log.WithLogger(ctx, log.G(ctx).WithField("digest", desc.Digest))
43+
44+
hosts := r.filterHosts(HostCapabilityPull)
45+
if len(hosts) == 0 {
46+
return nil, errors.Wrap(errdefs.ErrNotFound, "no pull hosts")
5347
}
5448

55-
ctx, err = contextWithRepositoryScope(ctx, r.refspec, false)
49+
ctx, err := contextWithRepositoryScope(ctx, r.refspec, false)
5650
if err != nil {
5751
return nil, err
5852
}
5953

6054
return newHTTPReadSeeker(desc.Size, func(offset int64) (io.ReadCloser, error) {
61-
for _, u := range urls {
62-
rc, err := r.open(ctx, u, desc.MediaType, offset)
55+
// firstly try fetch via external urls
56+
for _, us := range desc.URLs {
57+
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", us))
58+
59+
u, err := url.Parse(us)
60+
if err != nil {
61+
log.G(ctx).WithError(err).Debug("failed to parse")
62+
continue
63+
}
64+
log.G(ctx).Debug("trying alternative url")
65+
66+
// Try this first, parse it
67+
host := RegistryHost{
68+
Client: http.DefaultClient,
69+
Host: u.Host,
70+
Scheme: u.Scheme,
71+
Path: u.Path,
72+
Capabilities: HostCapabilityPull,
73+
}
74+
req := r.request(host, http.MethodGet)
75+
// Strip namespace from base
76+
req.path = u.Path
77+
if u.RawQuery != "" {
78+
req.path = req.path + "?" + u.RawQuery
79+
}
80+
81+
rc, err := r.open(ctx, req, desc.MediaType, offset)
6382
if err != nil {
6483
if errdefs.IsNotFound(err) {
6584
continue // try one of the other urls.
@@ -71,29 +90,62 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
7190
return rc, nil
7291
}
7392

93+
// Try manifests endpoints for manifests types
94+
switch desc.MediaType {
95+
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
96+
images.MediaTypeDockerSchema1Manifest,
97+
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
98+
99+
for _, host := range r.hosts {
100+
req := r.request(host, http.MethodGet, "manifests", desc.Digest.String())
101+
102+
rc, err := r.open(ctx, req, desc.MediaType, offset)
103+
if err != nil {
104+
if errdefs.IsNotFound(err) {
105+
continue // try another host
106+
}
107+
108+
return nil, err
109+
}
110+
111+
return rc, nil
112+
}
113+
}
114+
115+
// Finally use blobs endpoints
116+
for _, host := range r.hosts {
117+
req := r.request(host, http.MethodGet, "blobs", desc.Digest.String())
118+
119+
rc, err := r.open(ctx, req, desc.MediaType, offset)
120+
if err != nil {
121+
if errdefs.IsNotFound(err) {
122+
continue // try another host
123+
}
124+
125+
return nil, err
126+
}
127+
128+
return rc, nil
129+
}
130+
74131
return nil, errors.Wrapf(errdefs.ErrNotFound,
75132
"could not fetch content descriptor %v (%v) from remote",
76133
desc.Digest, desc.MediaType)
77134

78135
})
79136
}
80137

81-
func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int64) (io.ReadCloser, error) {
82-
req, err := http.NewRequest(http.MethodGet, u, nil)
83-
if err != nil {
84-
return nil, err
85-
}
86-
87-
req.Header.Set("Accept", strings.Join([]string{mediatype, `*`}, ", "))
138+
func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string, offset int64) (io.ReadCloser, error) {
139+
req.header.Set("Accept", strings.Join([]string{mediatype, `*`}, ", "))
88140

89141
if offset > 0 {
90142
// Note: "Accept-Ranges: bytes" cannot be trusted as some endpoints
91143
// will return the header without supporting the range. The content
92144
// range must always be checked.
93-
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
145+
req.header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
94146
}
95147

96-
resp, err := r.doRequestWithRetries(ctx, req, nil)
148+
resp, err := req.doWithRetries(ctx, nil)
97149
if err != nil {
98150
return nil, err
99151
}
@@ -106,13 +158,13 @@ func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int
106158
defer resp.Body.Close()
107159

108160
if resp.StatusCode == http.StatusNotFound {
109-
return nil, errors.Wrapf(errdefs.ErrNotFound, "content at %v not found", u)
161+
return nil, errors.Wrapf(errdefs.ErrNotFound, "content at %v not found", req.String())
110162
}
111163
var registryErr errcode.Errors
112164
if err := json.NewDecoder(resp.Body).Decode(&registryErr); err != nil || registryErr.Len() < 1 {
113-
return nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
165+
return nil, errors.Errorf("unexpected status code %v: %v", req.String(), resp.Status)
114166
}
115-
return nil, errors.Errorf("unexpected status code %v: %s - Server message: %s", u, resp.Status, registryErr.Error())
167+
return nil, errors.Errorf("unexpected status code %v: %s - Server message: %s", req.String(), resp.Status, registryErr.Error())
116168
}
117169
if offset > 0 {
118170
cr := resp.Header.Get("content-range")
@@ -141,30 +193,3 @@ func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int
141193

142194
return resp.Body, nil
143195
}
144-
145-
// getV2URLPaths generates the candidate urls paths for the object based on the
146-
// set of hints and the provided object id. URLs are returned in the order of
147-
// most to least likely succeed.
148-
func (r *dockerFetcher) getV2URLPaths(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
149-
var urls []string
150-
151-
if len(desc.URLs) > 0 {
152-
// handle fetch via external urls.
153-
for _, u := range desc.URLs {
154-
log.G(ctx).WithField("url", u).Debug("adding alternative url")
155-
urls = append(urls, u)
156-
}
157-
}
158-
159-
switch desc.MediaType {
160-
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
161-
images.MediaTypeDockerSchema1Manifest,
162-
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
163-
urls = append(urls, r.url(path.Join("manifests", desc.Digest.String())))
164-
}
165-
166-
// always fallback to attempting to get the object out of the blobs store.
167-
urls = append(urls, r.url(path.Join("blobs", desc.Digest.String())))
168-
169-
return urls, nil
170-
}

0 commit comments

Comments
 (0)