If we detect a Docker-Distribution-Api-Version header indicating that
the registry speaks the V2 protocol, no fallback to V1 should take
place.
The same applies if a V2 registry operation succeeds while attempting a
push or pull.
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
... | ... |
@@ -49,7 +49,7 @@ type Puller interface { |
49 | 49 |
// Pull tries to pull the image referenced by `tag` |
50 | 50 |
// Pull returns an error if any, as well as a boolean that determines whether to retry Pull on the next configured endpoint. |
51 | 51 |
// |
52 |
- Pull(ctx context.Context, ref reference.Named) (fallback bool, err error) |
|
52 |
+ Pull(ctx context.Context, ref reference.Named) error |
|
53 | 53 |
} |
54 | 54 |
|
55 | 55 |
// newPuller returns a Puller interface that will pull from either a v1 or v2 |
... | ... |
@@ -108,8 +108,17 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo |
108 | 108 |
// returned and displayed, but if there was a v2 endpoint which supports pull-by-digest, then the last relevant |
109 | 109 |
// error is the ones from v2 endpoints not v1. |
110 | 110 |
discardNoSupportErrors bool |
111 |
+ |
|
112 |
+ // confirmedV2 is set to true if a pull attempt managed to |
|
113 |
+ // confirm that it was talking to a v2 registry. This will |
|
114 |
+ // prevent fallback to the v1 protocol. |
|
115 |
+ confirmedV2 bool |
|
111 | 116 |
) |
112 | 117 |
for _, endpoint := range endpoints { |
118 |
+ if confirmedV2 && endpoint.Version == registry.APIVersion1 { |
|
119 |
+ logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL) |
|
120 |
+ continue |
|
121 |
+ } |
|
113 | 122 |
logrus.Debugf("Trying to pull %s from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version) |
114 | 123 |
|
115 | 124 |
puller, err := newPuller(endpoint, repoInfo, imagePullConfig) |
... | ... |
@@ -117,13 +126,18 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo |
117 | 117 |
errors = append(errors, err.Error()) |
118 | 118 |
continue |
119 | 119 |
} |
120 |
- if fallback, err := puller.Pull(ctx, ref); err != nil { |
|
120 |
+ if err := puller.Pull(ctx, ref); err != nil { |
|
121 | 121 |
// Was this pull cancelled? If so, don't try to fall |
122 | 122 |
// back. |
123 |
+ fallback := false |
|
123 | 124 |
select { |
124 | 125 |
case <-ctx.Done(): |
125 |
- fallback = false |
|
126 | 126 |
default: |
127 |
+ if fallbackErr, ok := err.(fallbackError); ok { |
|
128 |
+ fallback = true |
|
129 |
+ confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 |
|
130 |
+ err = fallbackErr.err |
|
131 |
+ } |
|
127 | 132 |
} |
128 | 133 |
if fallback { |
129 | 134 |
if _, ok := err.(registry.ErrNoSupport); !ok { |
... | ... |
@@ -33,15 +33,15 @@ type v1Puller struct { |
33 | 33 |
session *registry.Session |
34 | 34 |
} |
35 | 35 |
|
36 |
-func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) (fallback bool, err error) { |
|
36 |
+func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error { |
|
37 | 37 |
if _, isCanonical := ref.(reference.Canonical); isCanonical { |
38 | 38 |
// Allowing fallback, because HTTPS v1 is before HTTP v2 |
39 |
- return true, registry.ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")} |
|
39 |
+ return fallbackError{err: registry.ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")}} |
|
40 | 40 |
} |
41 | 41 |
|
42 | 42 |
tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name) |
43 | 43 |
if err != nil { |
44 |
- return false, err |
|
44 |
+ return err |
|
45 | 45 |
} |
46 | 46 |
// Adds Docker-specific headers as well as user-specified headers (metaHeaders) |
47 | 47 |
tr := transport.NewTransport( |
... | ... |
@@ -53,21 +53,21 @@ func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) (fallback bool |
53 | 53 |
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders) |
54 | 54 |
if err != nil { |
55 | 55 |
logrus.Debugf("Could not get v1 endpoint: %v", err) |
56 |
- return true, err |
|
56 |
+ return fallbackError{err: err} |
|
57 | 57 |
} |
58 | 58 |
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint) |
59 | 59 |
if err != nil { |
60 | 60 |
// TODO(dmcgowan): Check if should fallback |
61 | 61 |
logrus.Debugf("Fallback from error: %s", err) |
62 |
- return true, err |
|
62 |
+ return fallbackError{err: err} |
|
63 | 63 |
} |
64 | 64 |
if err := p.pullRepository(ctx, ref); err != nil { |
65 | 65 |
// TODO(dmcgowan): Check if should fallback |
66 |
- return false, err |
|
66 |
+ return err |
|
67 | 67 |
} |
68 | 68 |
progress.Message(p.config.ProgressOutput, "", p.repoInfo.FullName()+": this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.") |
69 | 69 |
|
70 |
- return false, nil |
|
70 |
+ return nil |
|
71 | 71 |
} |
72 | 72 |
|
73 | 73 |
func (p *v1Puller) pullRepository(ctx context.Context, ref reference.Named) error { |
... | ... |
@@ -32,24 +32,26 @@ type v2Puller struct { |
32 | 32 |
config *ImagePullConfig |
33 | 33 |
repoInfo *registry.RepositoryInfo |
34 | 34 |
repo distribution.Repository |
35 |
+ // confirmedV2 is set to true if we confirm we're talking to a v2 |
|
36 |
+ // registry. This is used to limit fallbacks to the v1 protocol. |
|
37 |
+ confirmedV2 bool |
|
35 | 38 |
} |
36 | 39 |
|
37 |
-func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (fallback bool, err error) { |
|
40 |
+func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) { |
|
38 | 41 |
// TODO(tiborvass): was ReceiveTimeout |
39 |
- p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") |
|
42 |
+ p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") |
|
40 | 43 |
if err != nil { |
41 | 44 |
logrus.Warnf("Error getting v2 registry: %v", err) |
42 |
- return true, err |
|
45 |
+ return fallbackError{err: err, confirmedV2: p.confirmedV2} |
|
43 | 46 |
} |
44 | 47 |
|
45 |
- if err := p.pullV2Repository(ctx, ref); err != nil { |
|
48 |
+ if err = p.pullV2Repository(ctx, ref); err != nil { |
|
46 | 49 |
if registry.ContinueOnError(err) { |
47 | 50 |
logrus.Debugf("Error trying v2 registry: %v", err) |
48 |
- return true, err |
|
51 |
+ return fallbackError{err: err, confirmedV2: p.confirmedV2} |
|
49 | 52 |
} |
50 |
- return false, err |
|
51 | 53 |
} |
52 |
- return false, nil |
|
54 |
+ return err |
|
53 | 55 |
} |
54 | 56 |
|
55 | 57 |
func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (err error) { |
... | ... |
@@ -67,6 +69,10 @@ func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (e |
67 | 67 |
return err |
68 | 68 |
} |
69 | 69 |
|
70 |
+ // If this call succeeded, we can be confident that the |
|
71 |
+ // registry on the other side speaks the v2 protocol. |
|
72 |
+ p.confirmedV2 = true |
|
73 |
+ |
|
70 | 74 |
// This probably becomes a lot nicer after the manifest |
71 | 75 |
// refactor... |
72 | 76 |
for _, tag := range tags { |
... | ... |
@@ -208,6 +214,11 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdat |
208 | 208 |
if unverifiedManifest == nil { |
209 | 209 |
return false, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest) |
210 | 210 |
} |
211 |
+ |
|
212 |
+ // If GetByTag succeeded, we can be confident that the registry on |
|
213 |
+ // the other side speaks the v2 protocol. |
|
214 |
+ p.confirmedV2 = true |
|
215 |
+ |
|
211 | 216 |
var verifiedManifest *schema1.Manifest |
212 | 217 |
verifiedManifest, err = verifyManifest(unverifiedManifest, ref) |
213 | 218 |
if err != nil { |
... | ... |
@@ -59,7 +59,7 @@ type Pusher interface { |
59 | 59 |
// Push returns an error if any, as well as a boolean that determines whether to retry Push on the next configured endpoint. |
60 | 60 |
// |
61 | 61 |
// TODO(tiborvass): have Push() take a reference to repository + tag, so that the pusher itself is repository-agnostic. |
62 |
- Push(ctx context.Context) (fallback bool, err error) |
|
62 |
+ Push(ctx context.Context) error |
|
63 | 63 |
} |
64 | 64 |
|
65 | 65 |
const compressionBufSize = 32768 |
... | ... |
@@ -116,8 +116,21 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo |
116 | 116 |
return fmt.Errorf("Repository does not exist: %s", repoInfo.Name()) |
117 | 117 |
} |
118 | 118 |
|
119 |
- var lastErr error |
|
119 |
+ var ( |
|
120 |
+ lastErr error |
|
121 |
+ |
|
122 |
+ // confirmedV2 is set to true if a push attempt managed to |
|
123 |
+ // confirm that it was talking to a v2 registry. This will |
|
124 |
+ // prevent fallback to the v1 protocol. |
|
125 |
+ confirmedV2 bool |
|
126 |
+ ) |
|
127 |
+ |
|
120 | 128 |
for _, endpoint := range endpoints { |
129 |
+ if confirmedV2 && endpoint.Version == registry.APIVersion1 { |
|
130 |
+ logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL) |
|
131 |
+ continue |
|
132 |
+ } |
|
133 |
+ |
|
121 | 134 |
logrus.Debugf("Trying to push %s to %s %s", repoInfo.FullName(), endpoint.URL, endpoint.Version) |
122 | 135 |
|
123 | 136 |
pusher, err := NewPusher(ref, endpoint, repoInfo, imagePushConfig) |
... | ... |
@@ -125,22 +138,22 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo |
125 | 125 |
lastErr = err |
126 | 126 |
continue |
127 | 127 |
} |
128 |
- if fallback, err := pusher.Push(ctx); err != nil { |
|
128 |
+ if err := pusher.Push(ctx); err != nil { |
|
129 | 129 |
// Was this push cancelled? If so, don't try to fall |
130 | 130 |
// back. |
131 | 131 |
select { |
132 | 132 |
case <-ctx.Done(): |
133 |
- fallback = false |
|
134 | 133 |
default: |
134 |
+ if fallbackErr, ok := err.(fallbackError); ok { |
|
135 |
+ confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 |
|
136 |
+ err = fallbackErr.err |
|
137 |
+ lastErr = err |
|
138 |
+ continue |
|
139 |
+ } |
|
135 | 140 |
} |
136 | 141 |
|
137 |
- if fallback { |
|
138 |
- lastErr = err |
|
139 |
- continue |
|
140 |
- } |
|
141 | 142 |
logrus.Debugf("Not continuing with error: %v", err) |
142 | 143 |
return err |
143 |
- |
|
144 | 144 |
} |
145 | 145 |
|
146 | 146 |
imagePushConfig.EventsService.Log("push", repoInfo.Name(), "") |
... | ... |
@@ -29,10 +29,10 @@ type v1Pusher struct { |
29 | 29 |
session *registry.Session |
30 | 30 |
} |
31 | 31 |
|
32 |
-func (p *v1Pusher) Push(ctx context.Context) (fallback bool, err error) { |
|
32 |
+func (p *v1Pusher) Push(ctx context.Context) error { |
|
33 | 33 |
tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name) |
34 | 34 |
if err != nil { |
35 |
- return false, err |
|
35 |
+ return err |
|
36 | 36 |
} |
37 | 37 |
// Adds Docker-specific headers as well as user-specified headers (metaHeaders) |
38 | 38 |
tr := transport.NewTransport( |
... | ... |
@@ -44,18 +44,18 @@ func (p *v1Pusher) Push(ctx context.Context) (fallback bool, err error) { |
44 | 44 |
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders) |
45 | 45 |
if err != nil { |
46 | 46 |
logrus.Debugf("Could not get v1 endpoint: %v", err) |
47 |
- return true, err |
|
47 |
+ return fallbackError{err: err} |
|
48 | 48 |
} |
49 | 49 |
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint) |
50 | 50 |
if err != nil { |
51 | 51 |
// TODO(dmcgowan): Check if should fallback |
52 |
- return true, err |
|
52 |
+ return fallbackError{err: err} |
|
53 | 53 |
} |
54 | 54 |
if err := p.pushRepository(ctx); err != nil { |
55 | 55 |
// TODO(dmcgowan): Check if should fallback |
56 |
- return false, err |
|
56 |
+ return err |
|
57 | 57 |
} |
58 |
- return false, nil |
|
58 |
+ return nil |
|
59 | 59 |
} |
60 | 60 |
|
61 | 61 |
// v1Image exposes the configuration, filesystem layer ID, and a v1 ID for an |
... | ... |
@@ -34,6 +34,10 @@ type v2Pusher struct { |
34 | 34 |
config *ImagePushConfig |
35 | 35 |
repo distribution.Repository |
36 | 36 |
|
37 |
+ // confirmedV2 is set to true if we confirm we're talking to a v2 |
|
38 |
+ // registry. This is used to limit fallbacks to the v1 protocol. |
|
39 |
+ confirmedV2 bool |
|
40 |
+ |
|
37 | 41 |
// layersPushed is the set of layers known to exist on the remote side. |
38 | 42 |
// This avoids redundant queries when pushing multiple tags that |
39 | 43 |
// involve the same layers. |
... | ... |
@@ -45,18 +49,27 @@ type pushMap struct { |
45 | 45 |
layersPushed map[digest.Digest]bool |
46 | 46 |
} |
47 | 47 |
|
48 |
-func (p *v2Pusher) Push(ctx context.Context) (fallback bool, err error) { |
|
49 |
- p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull") |
|
48 |
+func (p *v2Pusher) Push(ctx context.Context) (err error) { |
|
49 |
+ p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull") |
|
50 | 50 |
if err != nil { |
51 | 51 |
logrus.Debugf("Error getting v2 registry: %v", err) |
52 |
- return true, err |
|
52 |
+ return fallbackError{err: err, confirmedV2: p.confirmedV2} |
|
53 | 53 |
} |
54 | 54 |
|
55 |
+ if err = p.pushV2Repository(ctx); err != nil { |
|
56 |
+ if registry.ContinueOnError(err) { |
|
57 |
+ return fallbackError{err: err, confirmedV2: p.confirmedV2} |
|
58 |
+ } |
|
59 |
+ } |
|
60 |
+ return err |
|
61 |
+} |
|
62 |
+ |
|
63 |
+func (p *v2Pusher) pushV2Repository(ctx context.Context) (err error) { |
|
55 | 64 |
var associations []reference.Association |
56 | 65 |
if _, isTagged := p.ref.(reference.NamedTagged); isTagged { |
57 | 66 |
imageID, err := p.config.ReferenceStore.Get(p.ref) |
58 | 67 |
if err != nil { |
59 |
- return false, fmt.Errorf("tag does not exist: %s", p.ref.String()) |
|
68 |
+ return fmt.Errorf("tag does not exist: %s", p.ref.String()) |
|
60 | 69 |
} |
61 | 70 |
|
62 | 71 |
associations = []reference.Association{ |
... | ... |
@@ -70,19 +83,19 @@ func (p *v2Pusher) Push(ctx context.Context) (fallback bool, err error) { |
70 | 70 |
associations = p.config.ReferenceStore.ReferencesByName(p.ref) |
71 | 71 |
} |
72 | 72 |
if err != nil { |
73 |
- return false, fmt.Errorf("error getting tags for %s: %s", p.repoInfo.Name(), err) |
|
73 |
+ return fmt.Errorf("error getting tags for %s: %s", p.repoInfo.Name(), err) |
|
74 | 74 |
} |
75 | 75 |
if len(associations) == 0 { |
76 |
- return false, fmt.Errorf("no tags to push for %s", p.repoInfo.Name()) |
|
76 |
+ return fmt.Errorf("no tags to push for %s", p.repoInfo.Name()) |
|
77 | 77 |
} |
78 | 78 |
|
79 | 79 |
for _, association := range associations { |
80 | 80 |
if err := p.pushV2Tag(ctx, association); err != nil { |
81 |
- return false, err |
|
81 |
+ return err |
|
82 | 82 |
} |
83 | 83 |
} |
84 | 84 |
|
85 |
- return false, nil |
|
85 |
+ return nil |
|
86 | 86 |
} |
87 | 87 |
|
88 | 88 |
func (p *v2Pusher) pushV2Tag(ctx context.Context, association reference.Association) error { |
... | ... |
@@ -109,30 +122,28 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, association reference.Associat |
109 | 109 |
|
110 | 110 |
var descriptors []xfer.UploadDescriptor |
111 | 111 |
|
112 |
+ descriptorTemplate := v2PushDescriptor{ |
|
113 |
+ blobSumService: p.blobSumService, |
|
114 |
+ repo: p.repo, |
|
115 |
+ layersPushed: &p.layersPushed, |
|
116 |
+ confirmedV2: &p.confirmedV2, |
|
117 |
+ } |
|
118 |
+ |
|
112 | 119 |
// Push empty layer if necessary |
113 | 120 |
for _, h := range img.History { |
114 | 121 |
if h.EmptyLayer { |
115 |
- descriptors = []xfer.UploadDescriptor{ |
|
116 |
- &v2PushDescriptor{ |
|
117 |
- layer: layer.EmptyLayer, |
|
118 |
- blobSumService: p.blobSumService, |
|
119 |
- repo: p.repo, |
|
120 |
- layersPushed: &p.layersPushed, |
|
121 |
- }, |
|
122 |
- } |
|
122 |
+ descriptor := descriptorTemplate |
|
123 |
+ descriptor.layer = layer.EmptyLayer |
|
124 |
+ descriptors = []xfer.UploadDescriptor{&descriptor} |
|
123 | 125 |
break |
124 | 126 |
} |
125 | 127 |
} |
126 | 128 |
|
127 | 129 |
// Loop bounds condition is to avoid pushing the base layer on Windows. |
128 | 130 |
for i := 0; i < len(img.RootFS.DiffIDs); i++ { |
129 |
- descriptor := &v2PushDescriptor{ |
|
130 |
- layer: l, |
|
131 |
- blobSumService: p.blobSumService, |
|
132 |
- repo: p.repo, |
|
133 |
- layersPushed: &p.layersPushed, |
|
134 |
- } |
|
135 |
- descriptors = append(descriptors, descriptor) |
|
131 |
+ descriptor := descriptorTemplate |
|
132 |
+ descriptor.layer = l |
|
133 |
+ descriptors = append(descriptors, &descriptor) |
|
136 | 134 |
|
137 | 135 |
l = l.Parent() |
138 | 136 |
} |
... | ... |
@@ -181,6 +192,7 @@ type v2PushDescriptor struct { |
181 | 181 |
blobSumService *metadata.BlobSumService |
182 | 182 |
repo distribution.Repository |
183 | 183 |
layersPushed *pushMap |
184 |
+ confirmedV2 *bool |
|
184 | 185 |
} |
185 | 186 |
|
186 | 187 |
func (pd *v2PushDescriptor) Key() string { |
... | ... |
@@ -251,6 +263,10 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress. |
251 | 251 |
return "", retryOnError(err) |
252 | 252 |
} |
253 | 253 |
|
254 |
+ // If Commit succeded, that's an indication that the remote registry |
|
255 |
+ // speaks the v2 protocol. |
|
256 |
+ *pd.confirmedV2 = true |
|
257 |
+ |
|
254 | 258 |
logrus.Debugf("uploaded layer %s (%s), %d bytes", diffID, pushDigest, nn) |
255 | 259 |
progress.Update(progressOutput, pd.ID(), "Pushed") |
256 | 260 |
|
... | ... |
@@ -1,7 +1,6 @@ |
1 | 1 |
package distribution |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "errors" |
|
5 | 4 |
"fmt" |
6 | 5 |
"net" |
7 | 6 |
"net/http" |
... | ... |
@@ -24,6 +23,22 @@ import ( |
24 | 24 |
"golang.org/x/net/context" |
25 | 25 |
) |
26 | 26 |
|
27 |
+// fallbackError wraps an error that can possibly allow fallback to a different |
|
28 |
+// endpoint. |
|
29 |
+type fallbackError struct { |
|
30 |
+ // err is the error being wrapped. |
|
31 |
+ err error |
|
32 |
+ // confirmedV2 is set to true if it was confirmed that the registry |
|
33 |
+ // supports the v2 protocol. This is used to limit fallbacks to the v1 |
|
34 |
+ // protocol. |
|
35 |
+ confirmedV2 bool |
|
36 |
+} |
|
37 |
+ |
|
38 |
+// Error renders the FallbackError as a string. |
|
39 |
+func (f fallbackError) Error() string { |
|
40 |
+ return f.err.Error() |
|
41 |
+} |
|
42 |
+ |
|
27 | 43 |
type dumbCredentialStore struct { |
28 | 44 |
auth *types.AuthConfig |
29 | 45 |
} |
... | ... |
@@ -35,9 +50,7 @@ func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) { |
35 | 35 |
// NewV2Repository returns a repository (v2 only). It creates a HTTP transport |
36 | 36 |
// providing timeout settings and authentication support, and also verifies the |
37 | 37 |
// remote API version. |
38 |
-func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *types.AuthConfig, actions ...string) (distribution.Repository, error) { |
|
39 |
- ctx := context.Background() |
|
40 |
- |
|
38 |
+func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *types.AuthConfig, actions ...string) (repo distribution.Repository, foundVersion bool, err error) { |
|
41 | 39 |
repoName := repoInfo.FullName() |
42 | 40 |
// If endpoint does not support CanonicalName, use the RemoteName instead |
43 | 41 |
if endpoint.TrimHostname { |
... | ... |
@@ -67,32 +80,34 @@ func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd |
67 | 67 |
endpointStr := strings.TrimRight(endpoint.URL, "/") + "/v2/" |
68 | 68 |
req, err := http.NewRequest("GET", endpointStr, nil) |
69 | 69 |
if err != nil { |
70 |
- return nil, err |
|
70 |
+ return nil, false, err |
|
71 | 71 |
} |
72 | 72 |
resp, err := pingClient.Do(req) |
73 | 73 |
if err != nil { |
74 |
- return nil, err |
|
74 |
+ return nil, false, err |
|
75 | 75 |
} |
76 | 76 |
defer resp.Body.Close() |
77 | 77 |
|
78 |
- versions := auth.APIVersions(resp, endpoint.VersionHeader) |
|
79 |
- if endpoint.VersionHeader != "" && len(endpoint.Versions) > 0 { |
|
80 |
- var foundVersion bool |
|
81 |
- for _, version := range endpoint.Versions { |
|
82 |
- for _, pingVersion := range versions { |
|
83 |
- if version == pingVersion { |
|
84 |
- foundVersion = true |
|
85 |
- } |
|
86 |
- } |
|
87 |
- } |
|
88 |
- if !foundVersion { |
|
89 |
- return nil, errors.New("endpoint does not support v2 API") |
|
78 |
+ v2Version := auth.APIVersion{ |
|
79 |
+ Type: "registry", |
|
80 |
+ Version: "2.0", |
|
81 |
+ } |
|
82 |
+ |
|
83 |
+ versions := auth.APIVersions(resp, registry.DefaultRegistryVersionHeader) |
|
84 |
+ for _, pingVersion := range versions { |
|
85 |
+ if pingVersion == v2Version { |
|
86 |
+ // The version header indicates we're definitely |
|
87 |
+ // talking to a v2 registry. So don't allow future |
|
88 |
+ // fallbacks to the v1 protocol. |
|
89 |
+ |
|
90 |
+ foundVersion = true |
|
91 |
+ break |
|
90 | 92 |
} |
91 | 93 |
} |
92 | 94 |
|
93 | 95 |
challengeManager := auth.NewSimpleChallengeManager() |
94 | 96 |
if err := challengeManager.AddResponse(resp); err != nil { |
95 |
- return nil, err |
|
97 |
+ return nil, foundVersion, err |
|
96 | 98 |
} |
97 | 99 |
|
98 | 100 |
if authConfig.RegistryToken != "" { |
... | ... |
@@ -106,7 +121,8 @@ func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd |
106 | 106 |
} |
107 | 107 |
tr := transport.NewTransport(base, modifiers...) |
108 | 108 |
|
109 |
- return client.NewRepository(ctx, repoName, endpoint.URL, tr) |
|
109 |
+ repo, err = client.NewRepository(ctx, repoName, endpoint.URL, tr) |
|
110 |
+ return repo, foundVersion, err |
|
110 | 111 |
} |
111 | 112 |
|
112 | 113 |
func digestFromManifest(m *schema1.SignedManifest, name reference.Named) (digest.Digest, int, error) { |
... | ... |
@@ -147,7 +163,8 @@ func retryOnError(err error) error { |
147 | 147 |
case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied: |
148 | 148 |
return xfer.DoNotRetry{Err: err} |
149 | 149 |
} |
150 |
- |
|
150 |
+ case *client.UnexpectedHTTPResponseError: |
|
151 |
+ return xfer.DoNotRetry{Err: err} |
|
151 | 152 |
} |
152 | 153 |
// let's be nice and fallback if the error is a completely |
153 | 154 |
// unexpected one. |
... | ... |
@@ -8,7 +8,6 @@ import ( |
8 | 8 |
"testing" |
9 | 9 |
|
10 | 10 |
"github.com/Sirupsen/logrus" |
11 |
- "github.com/docker/distribution/registry/client/auth" |
|
12 | 11 |
"github.com/docker/docker/api/types" |
13 | 12 |
registrytypes "github.com/docker/docker/api/types/registry" |
14 | 13 |
"github.com/docker/docker/reference" |
... | ... |
@@ -49,12 +48,6 @@ func TestTokenPassThru(t *testing.T) { |
49 | 49 |
TrimHostname: false, |
50 | 50 |
TLSConfig: nil, |
51 | 51 |
//VersionHeader: "verheader", |
52 |
- Versions: []auth.APIVersion{ |
|
53 |
- { |
|
54 |
- Type: "registry", |
|
55 |
- Version: "2", |
|
56 |
- }, |
|
57 |
- }, |
|
58 | 52 |
} |
59 | 53 |
n, _ := reference.ParseNamed("testremotename") |
60 | 54 |
repoInfo := ®istry.RepositoryInfo{ |
... | ... |
@@ -76,7 +69,8 @@ func TestTokenPassThru(t *testing.T) { |
76 | 76 |
t.Fatal(err) |
77 | 77 |
} |
78 | 78 |
p := puller.(*v2Puller) |
79 |
- p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") |
|
79 |
+ ctx := context.Background() |
|
80 |
+ p.repo, _, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") |
|
80 | 81 |
if err != nil { |
81 | 82 |
t.Fatal(err) |
82 | 83 |
} |
... | ... |
@@ -84,7 +78,7 @@ func TestTokenPassThru(t *testing.T) { |
84 | 84 |
logrus.Debug("About to pull") |
85 | 85 |
// We expect it to fail, since we haven't mock'd the full registry exchange in our handler above |
86 | 86 |
tag, _ := reference.WithTag(n, "tag_goes_here") |
87 |
- _ = p.pullV2Repository(context.Background(), tag) |
|
87 |
+ _ = p.pullV2Repository(ctx, tag) |
|
88 | 88 |
|
89 | 89 |
if !gotToken { |
90 | 90 |
t.Fatal("Failed to receive registry token") |
... | ... |
@@ -1,7 +1,6 @@ |
1 | 1 |
package main |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "fmt" |
|
5 | 4 |
"regexp" |
6 | 5 |
"strings" |
7 | 6 |
"time" |
... | ... |
@@ -54,7 +53,8 @@ func (s *DockerHubPullSuite) TestPullNonExistingImage(c *check.C) { |
54 | 54 |
} { |
55 | 55 |
out, err := s.CmdWithError("pull", e.Alias) |
56 | 56 |
c.Assert(err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", out)) |
57 |
- c.Assert(out, checker.Contains, fmt.Sprintf("Error: image %s not found", e.Repo), check.Commentf("expected image not found error messages")) |
|
57 |
+ // Hub returns 401 rather than 404 for nonexistent library/ repos. |
|
58 |
+ c.Assert(out, checker.Contains, "unauthorized: access to the requested resource is not authorized", check.Commentf("expected unauthorized error message")) |
|
58 | 59 |
} |
59 | 60 |
} |
60 | 61 |
|
... | ... |
@@ -6,7 +6,6 @@ import ( |
6 | 6 |
"net/url" |
7 | 7 |
"strings" |
8 | 8 |
|
9 |
- "github.com/docker/distribution/registry/client/auth" |
|
10 | 9 |
"github.com/docker/docker/api/types" |
11 | 10 |
registrytypes "github.com/docker/docker/api/types/registry" |
12 | 11 |
"github.com/docker/docker/reference" |
... | ... |
@@ -121,14 +120,12 @@ func (s *Service) ResolveIndex(name string) (*registrytypes.IndexInfo, error) { |
121 | 121 |
|
122 | 122 |
// APIEndpoint represents a remote API endpoint |
123 | 123 |
type APIEndpoint struct { |
124 |
- Mirror bool |
|
125 |
- URL string |
|
126 |
- Version APIVersion |
|
127 |
- Official bool |
|
128 |
- TrimHostname bool |
|
129 |
- TLSConfig *tls.Config |
|
130 |
- VersionHeader string |
|
131 |
- Versions []auth.APIVersion |
|
124 |
+ Mirror bool |
|
125 |
+ URL string |
|
126 |
+ Version APIVersion |
|
127 |
+ Official bool |
|
128 |
+ TrimHostname bool |
|
129 |
+ TLSConfig *tls.Config |
|
132 | 130 |
} |
133 | 131 |
|
134 | 132 |
// ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint |
... | ... |
@@ -4,7 +4,6 @@ import ( |
4 | 4 |
"fmt" |
5 | 5 |
"strings" |
6 | 6 |
|
7 |
- "github.com/docker/distribution/registry/client/auth" |
|
8 | 7 |
"github.com/docker/docker/pkg/tlsconfig" |
9 | 8 |
"github.com/docker/docker/reference" |
10 | 9 |
) |
... | ... |
@@ -52,20 +51,12 @@ func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEn |
52 | 52 |
return nil, err |
53 | 53 |
} |
54 | 54 |
|
55 |
- v2Versions := []auth.APIVersion{ |
|
56 |
- { |
|
57 |
- Type: "registry", |
|
58 |
- Version: "2.0", |
|
59 |
- }, |
|
60 |
- } |
|
61 | 55 |
endpoints = []APIEndpoint{ |
62 | 56 |
{ |
63 |
- URL: "https://" + hostname, |
|
64 |
- Version: APIVersion2, |
|
65 |
- TrimHostname: true, |
|
66 |
- TLSConfig: tlsConfig, |
|
67 |
- VersionHeader: DefaultRegistryVersionHeader, |
|
68 |
- Versions: v2Versions, |
|
57 |
+ URL: "https://" + hostname, |
|
58 |
+ Version: APIVersion2, |
|
59 |
+ TrimHostname: true, |
|
60 |
+ TLSConfig: tlsConfig, |
|
69 | 61 |
}, |
70 | 62 |
} |
71 | 63 |
|
... | ... |
@@ -75,9 +66,7 @@ func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEn |
75 | 75 |
Version: APIVersion2, |
76 | 76 |
TrimHostname: true, |
77 | 77 |
// used to check if supposed to be secure via InsecureSkipVerify |
78 |
- TLSConfig: tlsConfig, |
|
79 |
- VersionHeader: DefaultRegistryVersionHeader, |
|
80 |
- Versions: v2Versions, |
|
78 |
+ TLSConfig: tlsConfig, |
|
81 | 79 |
}) |
82 | 80 |
} |
83 | 81 |
|