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 |
|