Browse code

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>

Aaron Lehmann authored on 2015/12/24 08:21:43
Showing 3 changed files
... ...
@@ -47,6 +47,9 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
47 47
 	}
48 48
 
49 49
 	if err = p.pullV2Repository(ctx, ref); err != nil {
50
+		if _, ok := err.(fallbackError); ok {
51
+			return err
52
+		}
50 53
 		if registry.ContinueOnError(err) {
51 54
 			logrus.Debugf("Error trying v2 registry: %v", err)
52 55
 			return fallbackError{err: err, confirmedV2: p.confirmedV2}
... ...
@@ -56,9 +59,13 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
56 56
 }
57 57
 
58 58
 func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (err error) {
59
-	var refs []reference.Named
59
+	var layersDownloaded bool
60 60
 	if !reference.IsNameOnly(ref) {
61
-		refs = []reference.Named{ref}
61
+		var err error
62
+		layersDownloaded, err = p.pullV2Tag(ctx, ref)
63
+		if err != nil {
64
+			return err
65
+		}
62 66
 	} else {
63 67
 		manSvc, err := p.repo.Manifests(ctx)
64 68
 		if err != nil {
... ...
@@ -67,11 +74,14 @@ func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (e
67 67
 
68 68
 		tags, err := manSvc.Tags()
69 69
 		if err != nil {
70
-			return err
70
+			// If this repository doesn't exist on V2, we should
71
+			// permit a fallback to V1.
72
+			return allowV1Fallback(err)
71 73
 		}
72 74
 
73
-		// If this call succeeded, we can be confident that the
74
-		// registry on the other side speaks the v2 protocol.
75
+		// The v2 registry knows about this repository, so we will not
76
+		// allow fallback to the v1 protocol even if we encounter an
77
+		// error later on.
75 78
 		p.confirmedV2 = true
76 79
 
77 80
 		// This probably becomes a lot nicer after the manifest
... ...
@@ -81,19 +91,20 @@ func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (e
81 81
 			if err != nil {
82 82
 				return err
83 83
 			}
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
84
+			pulledNew, err := p.pullV2Tag(ctx, tagRef)
85
+			if err != nil {
86
+				// Since this is the pull-all-tags case, don't
87
+				// allow an error pulling a particular tag to
88
+				// make the whole pull fall back to v1.
89
+				if fallbackErr, ok := err.(fallbackError); ok {
90
+					return fallbackErr.err
91
+				}
92
+				return err
93
+			}
94
+			// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged
95
+			// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?
96
+			layersDownloaded = layersDownloaded || pulledNew
95 97
 		}
96
-		layersDownloaded = layersDownloaded || pulledNew
97 98
 	}
98 99
 
99 100
 	writeStatus(ref.String(), p.config.ProgressOutput, layersDownloaded)
... ...
@@ -214,20 +225,7 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat
214 214
 		// fallback to the v1 protocol, because dual-version setups may
215 215
 		// not host all manifests with the v2 protocol. We may also get
216 216
 		// 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
217
+		return false, allowV1Fallback(err)
231 218
 	}
232 219
 	if unverifiedManifest == nil {
233 220
 		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
334 334
 	return true, nil
335 335
 }
336 336
 
337
+// allowV1Fallback checks if the error is a possible reason to fallback to v1
338
+// (even if confirmedV2 has been set already), and if so, wraps the error in
339
+// a fallbackError with confirmedV2 set to false. Otherwise, it returns the
340
+// error unmodified.
341
+func allowV1Fallback(err error) error {
342
+	switch v := err.(type) {
343
+	case errcode.Errors:
344
+		if len(v) != 0 {
345
+			if v0, ok := v[0].(errcode.Error); ok && registry.ShouldV2Fallback(v0) {
346
+				return fallbackError{err: err, confirmedV2: false}
347
+			}
348
+		}
349
+	case errcode.Error:
350
+		if registry.ShouldV2Fallback(v) {
351
+			return fallbackError{err: err, confirmedV2: false}
352
+		}
353
+	}
354
+
355
+	return err
356
+}
357
+
337 358
 func verifyManifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) {
338 359
 	// If pull by digest, then verify the manifest digest. NOTE: It is
339 360
 	// important to do this first, before any other content validation. If the
... ...
@@ -58,7 +58,15 @@ func (s *DockerHubPullSuite) TestPullNonExistingImage(c *check.C) {
58 58
 		// the v2 protocol - but we should end up falling back to v1,
59 59
 		// which does return a 404.
60 60
 		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
+		}
61 68
 	}
69
+
62 70
 }
63 71
 
64 72
 // TestPullFromCentralRegistryImplicitRefParts pulls an image from the central registry and verifies
... ...
@@ -191,7 +191,7 @@ func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
191 191
 // ShouldV2Fallback returns true if this error is a reason to fall back to v1.
192 192
 func ShouldV2Fallback(err errcode.Error) bool {
193 193
 	switch err.Code {
194
-	case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
194
+	case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
195 195
 		return true
196 196
 	}
197 197
 	return false