Skip to content

Commit c4697a8

Browse files
authored
Merge pull request #4007 from awprice/issue-3868-backport
[release/1.3 backport] Improve host fallback behaviour in docker remote
2 parents d76c121 + 37b9a34 commit c4697a8

3 files changed

Lines changed: 112 additions & 12 deletions

File tree

remotes/docker/fetcher.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,41 +96,49 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
9696
images.MediaTypeDockerSchema1Manifest,
9797
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
9898

99+
var firstErr error
99100
for _, host := range r.hosts {
100101
req := r.request(host, http.MethodGet, "manifests", desc.Digest.String())
101102

102103
rc, err := r.open(ctx, req, desc.MediaType, offset)
103104
if err != nil {
104-
if errdefs.IsNotFound(err) {
105-
continue // try another host
105+
// Store the error for referencing later
106+
if firstErr == nil {
107+
firstErr = err
106108
}
107-
108-
return nil, err
109+
continue // try another host
109110
}
110111

111112
return rc, nil
112113
}
114+
115+
return nil, firstErr
113116
}
114117

115118
// Finally use blobs endpoints
119+
var firstErr error
116120
for _, host := range r.hosts {
117121
req := r.request(host, http.MethodGet, "blobs", desc.Digest.String())
118122

119123
rc, err := r.open(ctx, req, desc.MediaType, offset)
120124
if err != nil {
121-
if errdefs.IsNotFound(err) {
122-
continue // try another host
125+
// Store the error for referencing later
126+
if firstErr == nil {
127+
firstErr = err
123128
}
124-
125-
return nil, err
129+
continue // try another host
126130
}
127131

128132
return rc, nil
129133
}
130134

131-
return nil, errors.Wrapf(errdefs.ErrNotFound,
132-
"could not fetch content descriptor %v (%v) from remote",
133-
desc.Digest, desc.MediaType)
135+
if errdefs.IsNotFound(firstErr) {
136+
firstErr = errors.Wrapf(errdefs.ErrNotFound,
137+
"could not fetch content descriptor %v (%v) from remote",
138+
desc.Digest, desc.MediaType)
139+
}
140+
141+
return nil, firstErr
134142

135143
})
136144
}

remotes/docker/resolver.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,11 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
286286
if errors.Cause(err) == ErrInvalidAuthorization {
287287
err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization")
288288
}
289-
return "", ocispec.Descriptor{}, err
289+
// Store the error for referencing later
290+
if lastErr == nil {
291+
lastErr = err
292+
}
293+
continue // try another host
290294
}
291295
resp.Body.Close() // don't care about body contents.
292296

remotes/docker/resolver_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"strconv"
3030
"strings"
3131
"testing"
32+
"time"
3233

3334
"github.com/containerd/containerd/remotes"
3435
digest "github.com/opencontainers/go-digest"
@@ -192,6 +193,93 @@ func TestBadTokenResolver(t *testing.T) {
192193
}
193194
}
194195

196+
func TestHostFailureFallbackResolver(t *testing.T) {
197+
sf := func(h http.Handler) (string, ResolverOptions, func()) {
198+
s := httptest.NewServer(h)
199+
base := s.URL[7:] // strip "http://"
200+
201+
options := ResolverOptions{}
202+
createHost := func(host string) RegistryHost {
203+
return RegistryHost{
204+
Client: &http.Client{
205+
// Set the timeout so we timeout waiting for the non-responsive HTTP server
206+
Timeout: 500 * time.Millisecond,
207+
},
208+
Host: host,
209+
Scheme: "http",
210+
Path: "/v2",
211+
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
212+
}
213+
}
214+
215+
// Create an unstarted HTTP server. We use this to generate a random port.
216+
notRunning := httptest.NewUnstartedServer(nil)
217+
notRunningBase := notRunning.Listener.Addr().String()
218+
219+
// Override hosts with two hosts
220+
options.Hosts = func(host string) ([]RegistryHost, error) {
221+
return []RegistryHost{
222+
createHost(notRunningBase), // This host IS running, but with a non-responsive HTTP server
223+
createHost(base), // This host IS running
224+
}, nil
225+
}
226+
227+
return base, options, s.Close
228+
}
229+
230+
runBasicTest(t, "testname", sf)
231+
}
232+
233+
func TestHostTLSFailureFallbackResolver(t *testing.T) {
234+
sf := func(h http.Handler) (string, ResolverOptions, func()) {
235+
// Start up two servers
236+
server := httptest.NewServer(h)
237+
httpBase := server.URL[7:] // strip "http://"
238+
239+
tlsServer := httptest.NewUnstartedServer(h)
240+
tlsServer.StartTLS()
241+
httpsBase := tlsServer.URL[8:] // strip "https://"
242+
243+
capool := x509.NewCertPool()
244+
cert, _ := x509.ParseCertificate(tlsServer.TLS.Certificates[0].Certificate[0])
245+
capool.AddCert(cert)
246+
247+
client := &http.Client{
248+
Transport: &http.Transport{
249+
TLSClientConfig: &tls.Config{
250+
RootCAs: capool,
251+
},
252+
},
253+
}
254+
255+
options := ResolverOptions{}
256+
createHost := func(host string) RegistryHost {
257+
return RegistryHost{
258+
Client: client,
259+
Host: host,
260+
Scheme: "https",
261+
Path: "/v2",
262+
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
263+
}
264+
}
265+
266+
// Override hosts with two hosts
267+
options.Hosts = func(host string) ([]RegistryHost, error) {
268+
return []RegistryHost{
269+
createHost(httpBase), // This host is serving plain HTTP
270+
createHost(httpsBase), // This host is serving TLS
271+
}, nil
272+
}
273+
274+
return httpBase, options, func() {
275+
server.Close()
276+
tlsServer.Close()
277+
}
278+
}
279+
280+
runBasicTest(t, "testname", sf)
281+
}
282+
195283
func withTokenServer(th http.Handler, creds func(string) (string, string, error)) func(h http.Handler) (string, ResolverOptions, func()) {
196284
return func(h http.Handler) (string, ResolverOptions, func()) {
197285
s := httptest.NewUnstartedServer(th)

0 commit comments

Comments
 (0)