Skip to content
This repository was archived by the owner on Feb 8, 2021. It is now read-only.

Commit 589a522

Browse files
committed
Allow v1 protocol fallback when pulling all tags from a repository unknown to v2 registry
This is a followup to #18839. That PR relaxed the fallback logic so that if a manifest doesn't exist on v2, or the user is unauthorized to access it, we try again with the v1 protocol. A similar special case is needed for "pull all tags" (docker pull -a). If the v2 registry doesn't recognize the repository, or doesn't allow the user to access it, we should fall back to v1 and try to pull all tags from the v1 registry. Conversely, if the v2 registry does allow us to list the tags, there should be no fallback, even if there are errors pulling those tags. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
1 parent 725eef3 commit 589a522

File tree

3 files changed

+59
-32
lines changed

3 files changed

+59
-32
lines changed

distribution/pull_v2.go

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
4747
}
4848

4949
if err = p.pullV2Repository(ctx, ref); err != nil {
50+
if _, ok := err.(fallbackError); ok {
51+
return err
52+
}
5053
if registry.ContinueOnError(err) {
5154
logrus.Debugf("Error trying v2 registry: %v", err)
5255
return fallbackError{err: err, confirmedV2: p.confirmedV2}
@@ -56,9 +59,13 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
5659
}
5760

5861
func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (err error) {
59-
var refs []reference.Named
62+
var layersDownloaded bool
6063
if !reference.IsNameOnly(ref) {
61-
refs = []reference.Named{ref}
64+
var err error
65+
layersDownloaded, err = p.pullV2Tag(ctx, ref)
66+
if err != nil {
67+
return err
68+
}
6269
} else {
6370
manSvc, err := p.repo.Manifests(ctx)
6471
if err != nil {
@@ -67,11 +74,14 @@ func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (e
6774

6875
tags, err := manSvc.Tags()
6976
if err != nil {
70-
return err
77+
// If this repository doesn't exist on V2, we should
78+
// permit a fallback to V1.
79+
return allowV1Fallback(err)
7180
}
7281

73-
// If this call succeeded, we can be confident that the
74-
// registry on the other side speaks the v2 protocol.
82+
// The v2 registry knows about this repository, so we will not
83+
// allow fallback to the v1 protocol even if we encounter an
84+
// error later on.
7585
p.confirmedV2 = true
7686

7787
// This probably becomes a lot nicer after the manifest
@@ -81,19 +91,20 @@ func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (e
8191
if err != nil {
8292
return err
8393
}
84-
refs = append(refs, tagRef)
85-
}
86-
}
87-
88-
var layersDownloaded bool
89-
for _, pullRef := range refs {
90-
// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged
91-
// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?
92-
pulledNew, err := p.pullV2Tag(ctx, pullRef)
93-
if err != nil {
94-
return err
94+
pulledNew, err := p.pullV2Tag(ctx, tagRef)
95+
if err != nil {
96+
// Since this is the pull-all-tags case, don't
97+
// allow an error pulling a particular tag to
98+
// make the whole pull fall back to v1.
99+
if fallbackErr, ok := err.(fallbackError); ok {
100+
return fallbackErr.err
101+
}
102+
return err
103+
}
104+
// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged
105+
// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?
106+
layersDownloaded = layersDownloaded || pulledNew
95107
}
96-
layersDownloaded = layersDownloaded || pulledNew
97108
}
98109

99110
writeStatus(ref.String(), p.config.ProgressOutput, layersDownloaded)
@@ -214,20 +225,7 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat
214225
// fallback to the v1 protocol, because dual-version setups may
215226
// not host all manifests with the v2 protocol. We may also get
216227
// a "not authorized" error if the manifest doesn't exist.
217-
switch v := err.(type) {
218-
case errcode.Errors:
219-
if len(v) != 0 {
220-
if v0, ok := v[0].(errcode.Error); ok && registry.ShouldV2Fallback(v0) {
221-
p.confirmedV2 = false
222-
}
223-
}
224-
case errcode.Error:
225-
if registry.ShouldV2Fallback(v) {
226-
p.confirmedV2 = false
227-
}
228-
}
229-
230-
return false, err
228+
return false, allowV1Fallback(err)
231229
}
232230
if unverifiedManifest == nil {
233231
return false, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest)
@@ -334,6 +332,27 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat
334332
return true, nil
335333
}
336334

335+
// allowV1Fallback checks if the error is a possible reason to fallback to v1
336+
// (even if confirmedV2 has been set already), and if so, wraps the error in
337+
// a fallbackError with confirmedV2 set to false. Otherwise, it returns the
338+
// error unmodified.
339+
func allowV1Fallback(err error) error {
340+
switch v := err.(type) {
341+
case errcode.Errors:
342+
if len(v) != 0 {
343+
if v0, ok := v[0].(errcode.Error); ok && registry.ShouldV2Fallback(v0) {
344+
return fallbackError{err: err, confirmedV2: false}
345+
}
346+
}
347+
case errcode.Error:
348+
if registry.ShouldV2Fallback(v) {
349+
return fallbackError{err: err, confirmedV2: false}
350+
}
351+
}
352+
353+
return err
354+
}
355+
337356
func verifyManifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) {
338357
// If pull by digest, then verify the manifest digest. NOTE: It is
339358
// important to do this first, before any other content validation. If the

integration-cli/docker_cli_pull_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,15 @@ func (s *DockerHubPullSuite) TestPullNonExistingImage(c *check.C) {
5858
// the v2 protocol - but we should end up falling back to v1,
5959
// which does return a 404.
6060
c.Assert(out, checker.Contains, fmt.Sprintf("Error: image %s not found", e.Repo), check.Commentf("expected image not found error messages"))
61+
62+
// pull -a on a nonexistent registry should fall back as well
63+
if !strings.ContainsRune(e.Alias, ':') {
64+
out, err := s.CmdWithError("pull", "-a", e.Alias)
65+
c.Assert(err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", out))
66+
c.Assert(out, checker.Contains, fmt.Sprintf("Error: image %s not found", e.Repo), check.Commentf("expected image not found error messages"))
67+
}
6168
}
69+
6270
}
6371

6472
// TestPullFromCentralRegistryImplicitRefParts pulls an image from the central registry and verifies

registry/registry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
191191
// ShouldV2Fallback returns true if this error is a reason to fall back to v1.
192192
func ShouldV2Fallback(err errcode.Error) bool {
193193
switch err.Code {
194-
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
194+
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
195195
return true
196196
}
197197
return false

0 commit comments

Comments
 (0)