Skip to content

Commit 0b29c9c

Browse files
committed
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 <[email protected]>
1 parent a0696b2 commit 0b29c9c

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)