Skip to content

Commit 37b9a34

Browse files
committed
Improve host fallback behaviour in docker remote
This commit improves the fallback behaviour when resolving and fetching images with multiple hosts. If an error is encountered when resolving and fetching images, and more than one host is being used, we will try the same operation on the next host. The error from the first host is preserved so that if all hosts fail, we can display the error from the first host. fixes #3850 Signed-off-by: Alex Price <[email protected]>
1 parent d76c121 commit 37b9a34

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)