Several improvements to error handling:
- Introduce ImageConfigPullError type, wrapping errors related to
downloading the image configuration blob in schema2. This allows for a
more descriptive error message to be seen by the end user.
- Change some logrus.Debugf calls that display errors to logrus.Errorf.
Add log lines in the push/pull fallback cases to make sure the errors
leading to the fallback are shown.
- Move error-related types and functions which are only used by the
distribution package out of the registry package.
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,102 @@ |
| 0 |
+package distribution |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "net/url" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "syscall" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/distribution/registry/api/errcode" |
|
| 8 |
+ "github.com/docker/distribution/registry/api/v2" |
|
| 9 |
+ "github.com/docker/distribution/registry/client" |
|
| 10 |
+ "github.com/docker/docker/distribution/xfer" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// ErrNoSupport is an error type used for errors indicating that an operation |
|
| 14 |
+// is not supported. It encapsulates a more specific error. |
|
| 15 |
+type ErrNoSupport struct{ Err error }
|
|
| 16 |
+ |
|
| 17 |
+func (e ErrNoSupport) Error() string {
|
|
| 18 |
+ if e.Err == nil {
|
|
| 19 |
+ return "not supported" |
|
| 20 |
+ } |
|
| 21 |
+ return e.Err.Error() |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+// fallbackError wraps an error that can possibly allow fallback to a different |
|
| 25 |
+// endpoint. |
|
| 26 |
+type fallbackError struct {
|
|
| 27 |
+ // err is the error being wrapped. |
|
| 28 |
+ err error |
|
| 29 |
+ // confirmedV2 is set to true if it was confirmed that the registry |
|
| 30 |
+ // supports the v2 protocol. This is used to limit fallbacks to the v1 |
|
| 31 |
+ // protocol. |
|
| 32 |
+ confirmedV2 bool |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+// Error renders the FallbackError as a string. |
|
| 36 |
+func (f fallbackError) Error() string {
|
|
| 37 |
+ return f.err.Error() |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// shouldV2Fallback returns true if this error is a reason to fall back to v1. |
|
| 41 |
+func shouldV2Fallback(err errcode.Error) bool {
|
|
| 42 |
+ switch err.Code {
|
|
| 43 |
+ case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown: |
|
| 44 |
+ return true |
|
| 45 |
+ } |
|
| 46 |
+ return false |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+// continueOnError returns true if we should fallback to the next endpoint |
|
| 50 |
+// as a result of this error. |
|
| 51 |
+func continueOnError(err error) bool {
|
|
| 52 |
+ switch v := err.(type) {
|
|
| 53 |
+ case errcode.Errors: |
|
| 54 |
+ if len(v) == 0 {
|
|
| 55 |
+ return true |
|
| 56 |
+ } |
|
| 57 |
+ return continueOnError(v[0]) |
|
| 58 |
+ case ErrNoSupport: |
|
| 59 |
+ return continueOnError(v.Err) |
|
| 60 |
+ case errcode.Error: |
|
| 61 |
+ return shouldV2Fallback(v) |
|
| 62 |
+ case *client.UnexpectedHTTPResponseError: |
|
| 63 |
+ return true |
|
| 64 |
+ case ImageConfigPullError: |
|
| 65 |
+ return false |
|
| 66 |
+ case error: |
|
| 67 |
+ return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) |
|
| 68 |
+ } |
|
| 69 |
+ // let's be nice and fallback if the error is a completely |
|
| 70 |
+ // unexpected one. |
|
| 71 |
+ // If new errors have to be handled in some way, please |
|
| 72 |
+ // add them to the switch above. |
|
| 73 |
+ return true |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+// retryOnError wraps the error in xfer.DoNotRetry if we should not retry the |
|
| 77 |
+// operation after this error. |
|
| 78 |
+func retryOnError(err error) error {
|
|
| 79 |
+ switch v := err.(type) {
|
|
| 80 |
+ case errcode.Errors: |
|
| 81 |
+ return retryOnError(v[0]) |
|
| 82 |
+ case errcode.Error: |
|
| 83 |
+ switch v.Code {
|
|
| 84 |
+ case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied: |
|
| 85 |
+ return xfer.DoNotRetry{Err: err}
|
|
| 86 |
+ } |
|
| 87 |
+ case *url.Error: |
|
| 88 |
+ return retryOnError(v.Err) |
|
| 89 |
+ case *client.UnexpectedHTTPResponseError: |
|
| 90 |
+ return xfer.DoNotRetry{Err: err}
|
|
| 91 |
+ case error: |
|
| 92 |
+ if strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) {
|
|
| 93 |
+ return xfer.DoNotRetry{Err: err}
|
|
| 94 |
+ } |
|
| 95 |
+ } |
|
| 96 |
+ // let's be nice and fallback if the error is a completely |
|
| 97 |
+ // unexpected one. |
|
| 98 |
+ // If new errors have to be handled in some way, please |
|
| 99 |
+ // add them to the switch above. |
|
| 100 |
+ return err |
|
| 101 |
+} |
| ... | ... |
@@ -136,7 +136,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo |
| 136 | 136 |
} |
| 137 | 137 |
} |
| 138 | 138 |
if fallback {
|
| 139 |
- if _, ok := err.(registry.ErrNoSupport); !ok {
|
|
| 139 |
+ if _, ok := err.(ErrNoSupport); !ok {
|
|
| 140 | 140 |
// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors. |
| 141 | 141 |
discardNoSupportErrors = true |
| 142 | 142 |
// append subsequent errors |
| ... | ... |
@@ -147,9 +147,10 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo |
| 147 | 147 |
// append subsequent errors |
| 148 | 148 |
lastErr = err |
| 149 | 149 |
} |
| 150 |
+ logrus.Errorf("Attempting next endpoint for pull after error: %v", err)
|
|
| 150 | 151 |
continue |
| 151 | 152 |
} |
| 152 |
- logrus.Debugf("Not continuing with error: %v", err)
|
|
| 153 |
+ logrus.Errorf("Not continuing with pull after error: %v", err)
|
|
| 153 | 154 |
return err |
| 154 | 155 |
} |
| 155 | 156 |
|
| ... | ... |
@@ -38,7 +38,7 @@ type v1Puller struct {
|
| 38 | 38 |
func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error {
|
| 39 | 39 |
if _, isCanonical := ref.(reference.Canonical); isCanonical {
|
| 40 | 40 |
// Allowing fallback, because HTTPS v1 is before HTTP v2 |
| 41 |
- return fallbackError{err: registry.ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")}}
|
|
| 41 |
+ return fallbackError{err: ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")}}
|
|
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 | 44 |
tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name) |
| ... | ... |
@@ -35,6 +35,17 @@ import ( |
| 35 | 35 |
|
| 36 | 36 |
var errRootFSMismatch = errors.New("layers from manifest don't match image configuration")
|
| 37 | 37 |
|
| 38 |
+// ImageConfigPullError is an error pulling the image config blob |
|
| 39 |
+// (only applies to schema2). |
|
| 40 |
+type ImageConfigPullError struct {
|
|
| 41 |
+ Err error |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+// Error returns the error string for ImageConfigPullError. |
|
| 45 |
+func (e ImageConfigPullError) Error() string {
|
|
| 46 |
+ return "error pulling image configuration: " + e.Err.Error() |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 38 | 49 |
type v2Puller struct {
|
| 39 | 50 |
V2MetadataService *metadata.V2MetadataService |
| 40 | 51 |
endpoint registry.APIEndpoint |
| ... | ... |
@@ -58,8 +69,8 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
|
| 58 | 58 |
if _, ok := err.(fallbackError); ok {
|
| 59 | 59 |
return err |
| 60 | 60 |
} |
| 61 |
- if registry.ContinueOnError(err) {
|
|
| 62 |
- logrus.Debugf("Error trying v2 registry: %v", err)
|
|
| 61 |
+ if continueOnError(err) {
|
|
| 62 |
+ logrus.Errorf("Error trying v2 registry: %v", err)
|
|
| 63 | 63 |
return fallbackError{err: err, confirmedV2: p.confirmedV2}
|
| 64 | 64 |
} |
| 65 | 65 |
} |
| ... | ... |
@@ -170,7 +181,7 @@ func (ld *v2LayerDescriptor) Download(ctx context.Context, progressOutput progre |
| 170 | 170 |
|
| 171 | 171 |
layerDownload, err := blobs.Open(ctx, ld.digest) |
| 172 | 172 |
if err != nil {
|
| 173 |
- logrus.Debugf("Error initiating layer download: %v", err)
|
|
| 173 |
+ logrus.Errorf("Error initiating layer download: %v", err)
|
|
| 174 | 174 |
if err == distribution.ErrBlobUnknown {
|
| 175 | 175 |
return nil, 0, xfer.DoNotRetry{Err: err}
|
| 176 | 176 |
} |
| ... | ... |
@@ -280,12 +291,12 @@ func (ld *v2LayerDescriptor) truncateDownloadFile() error {
|
| 280 | 280 |
ld.verifier = nil |
| 281 | 281 |
|
| 282 | 282 |
if _, err := ld.tmpFile.Seek(0, os.SEEK_SET); err != nil {
|
| 283 |
- logrus.Debugf("error seeking to beginning of download file: %v", err)
|
|
| 283 |
+ logrus.Errorf("error seeking to beginning of download file: %v", err)
|
|
| 284 | 284 |
return err |
| 285 | 285 |
} |
| 286 | 286 |
|
| 287 | 287 |
if err := ld.tmpFile.Truncate(0); err != nil {
|
| 288 |
- logrus.Debugf("error truncating download file: %v", err)
|
|
| 288 |
+ logrus.Errorf("error truncating download file: %v", err)
|
|
| 289 | 289 |
return err |
| 290 | 290 |
} |
| 291 | 291 |
|
| ... | ... |
@@ -484,7 +495,7 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s |
| 484 | 484 |
go func() {
|
| 485 | 485 |
configJSON, err := p.pullSchema2ImageConfig(ctx, target.Digest) |
| 486 | 486 |
if err != nil {
|
| 487 |
- errChan <- err |
|
| 487 |
+ errChan <- ImageConfigPullError{Err: err}
|
|
| 488 | 488 |
cancel() |
| 489 | 489 |
return |
| 490 | 490 |
} |
| ... | ... |
@@ -704,12 +715,12 @@ func allowV1Fallback(err error) error {
|
| 704 | 704 |
switch v := err.(type) {
|
| 705 | 705 |
case errcode.Errors: |
| 706 | 706 |
if len(v) != 0 {
|
| 707 |
- if v0, ok := v[0].(errcode.Error); ok && registry.ShouldV2Fallback(v0) {
|
|
| 707 |
+ if v0, ok := v[0].(errcode.Error); ok && shouldV2Fallback(v0) {
|
|
| 708 | 708 |
return fallbackError{err: err, confirmedV2: false}
|
| 709 | 709 |
} |
| 710 | 710 |
} |
| 711 | 711 |
case errcode.Error: |
| 712 |
- if registry.ShouldV2Fallback(v) {
|
|
| 712 |
+ if shouldV2Fallback(v) {
|
|
| 713 | 713 |
return fallbackError{err: err, confirmedV2: false}
|
| 714 | 714 |
} |
| 715 | 715 |
case *url.Error: |
| ... | ... |
@@ -144,11 +144,12 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo |
| 144 | 144 |
confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 |
| 145 | 145 |
err = fallbackErr.err |
| 146 | 146 |
lastErr = err |
| 147 |
+ logrus.Errorf("Attempting next endpoint for push after error: %v", err)
|
|
| 147 | 148 |
continue |
| 148 | 149 |
} |
| 149 | 150 |
} |
| 150 | 151 |
|
| 151 |
- logrus.Debugf("Not continuing with error: %v", err)
|
|
| 152 |
+ logrus.Errorf("Not continuing with push after error: %v", err)
|
|
| 152 | 153 |
return err |
| 153 | 154 |
} |
| 154 | 155 |
|
| ... | ... |
@@ -68,7 +68,7 @@ func (p *v2Pusher) Push(ctx context.Context) (err error) {
|
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 | 70 |
if err = p.pushV2Repository(ctx); err != nil {
|
| 71 |
- if registry.ContinueOnError(err) {
|
|
| 71 |
+ if continueOnError(err) {
|
|
| 72 | 72 |
return fallbackError{err: err, confirmedV2: p.pushState.confirmedV2}
|
| 73 | 73 |
} |
| 74 | 74 |
} |
| ... | ... |
@@ -6,38 +6,19 @@ import ( |
| 6 | 6 |
"net/http" |
| 7 | 7 |
"net/url" |
| 8 | 8 |
"strings" |
| 9 |
- "syscall" |
|
| 10 | 9 |
"time" |
| 11 | 10 |
|
| 12 | 11 |
"github.com/docker/distribution" |
| 13 | 12 |
distreference "github.com/docker/distribution/reference" |
| 14 |
- "github.com/docker/distribution/registry/api/errcode" |
|
| 15 | 13 |
"github.com/docker/distribution/registry/client" |
| 16 | 14 |
"github.com/docker/distribution/registry/client/auth" |
| 17 | 15 |
"github.com/docker/distribution/registry/client/transport" |
| 18 |
- "github.com/docker/docker/distribution/xfer" |
|
| 19 | 16 |
"github.com/docker/docker/dockerversion" |
| 20 | 17 |
"github.com/docker/docker/registry" |
| 21 | 18 |
"github.com/docker/engine-api/types" |
| 22 | 19 |
"golang.org/x/net/context" |
| 23 | 20 |
) |
| 24 | 21 |
|
| 25 |
-// fallbackError wraps an error that can possibly allow fallback to a different |
|
| 26 |
-// endpoint. |
|
| 27 |
-type fallbackError struct {
|
|
| 28 |
- // err is the error being wrapped. |
|
| 29 |
- err error |
|
| 30 |
- // confirmedV2 is set to true if it was confirmed that the registry |
|
| 31 |
- // supports the v2 protocol. This is used to limit fallbacks to the v1 |
|
| 32 |
- // protocol. |
|
| 33 |
- confirmedV2 bool |
|
| 34 |
-} |
|
| 35 |
- |
|
| 36 |
-// Error renders the FallbackError as a string. |
|
| 37 |
-func (f fallbackError) Error() string {
|
|
| 38 |
- return f.err.Error() |
|
| 39 |
-} |
|
| 40 |
- |
|
| 41 | 22 |
type dumbCredentialStore struct {
|
| 42 | 23 |
auth *types.AuthConfig |
| 43 | 24 |
} |
| ... | ... |
@@ -141,30 +122,3 @@ func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[s |
| 141 | 141 |
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token))
|
| 142 | 142 |
return nil |
| 143 | 143 |
} |
| 144 |
- |
|
| 145 |
-// retryOnError wraps the error in xfer.DoNotRetry if we should not retry the |
|
| 146 |
-// operation after this error. |
|
| 147 |
-func retryOnError(err error) error {
|
|
| 148 |
- switch v := err.(type) {
|
|
| 149 |
- case errcode.Errors: |
|
| 150 |
- return retryOnError(v[0]) |
|
| 151 |
- case errcode.Error: |
|
| 152 |
- switch v.Code {
|
|
| 153 |
- case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied: |
|
| 154 |
- return xfer.DoNotRetry{Err: err}
|
|
| 155 |
- } |
|
| 156 |
- case *url.Error: |
|
| 157 |
- return retryOnError(v.Err) |
|
| 158 |
- case *client.UnexpectedHTTPResponseError: |
|
| 159 |
- return xfer.DoNotRetry{Err: err}
|
|
| 160 |
- case error: |
|
| 161 |
- if strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) {
|
|
| 162 |
- return xfer.DoNotRetry{Err: err}
|
|
| 163 |
- } |
|
| 164 |
- } |
|
| 165 |
- // let's be nice and fallback if the error is a completely |
|
| 166 |
- // unexpected one. |
|
| 167 |
- // If new errors have to be handled in some way, please |
|
| 168 |
- // add them to the switch above. |
|
| 169 |
- return err |
|
| 170 |
-} |
| ... | ... |
@@ -13,13 +13,9 @@ import ( |
| 13 | 13 |
"path/filepath" |
| 14 | 14 |
"runtime" |
| 15 | 15 |
"strings" |
| 16 |
- "syscall" |
|
| 17 | 16 |
"time" |
| 18 | 17 |
|
| 19 | 18 |
"github.com/Sirupsen/logrus" |
| 20 |
- "github.com/docker/distribution/registry/api/errcode" |
|
| 21 |
- "github.com/docker/distribution/registry/api/v2" |
|
| 22 |
- "github.com/docker/distribution/registry/client" |
|
| 23 | 19 |
"github.com/docker/distribution/registry/client/transport" |
| 24 | 20 |
"github.com/docker/go-connections/tlsconfig" |
| 25 | 21 |
) |
| ... | ... |
@@ -169,51 +165,6 @@ func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque |
| 169 | 169 |
return nil |
| 170 | 170 |
} |
| 171 | 171 |
|
| 172 |
-// ShouldV2Fallback returns true if this error is a reason to fall back to v1. |
|
| 173 |
-func ShouldV2Fallback(err errcode.Error) bool {
|
|
| 174 |
- switch err.Code {
|
|
| 175 |
- case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown: |
|
| 176 |
- return true |
|
| 177 |
- } |
|
| 178 |
- return false |
|
| 179 |
-} |
|
| 180 |
- |
|
| 181 |
-// ErrNoSupport is an error type used for errors indicating that an operation |
|
| 182 |
-// is not supported. It encapsulates a more specific error. |
|
| 183 |
-type ErrNoSupport struct{ Err error }
|
|
| 184 |
- |
|
| 185 |
-func (e ErrNoSupport) Error() string {
|
|
| 186 |
- if e.Err == nil {
|
|
| 187 |
- return "not supported" |
|
| 188 |
- } |
|
| 189 |
- return e.Err.Error() |
|
| 190 |
-} |
|
| 191 |
- |
|
| 192 |
-// ContinueOnError returns true if we should fallback to the next endpoint |
|
| 193 |
-// as a result of this error. |
|
| 194 |
-func ContinueOnError(err error) bool {
|
|
| 195 |
- switch v := err.(type) {
|
|
| 196 |
- case errcode.Errors: |
|
| 197 |
- if len(v) == 0 {
|
|
| 198 |
- return true |
|
| 199 |
- } |
|
| 200 |
- return ContinueOnError(v[0]) |
|
| 201 |
- case ErrNoSupport: |
|
| 202 |
- return ContinueOnError(v.Err) |
|
| 203 |
- case errcode.Error: |
|
| 204 |
- return ShouldV2Fallback(v) |
|
| 205 |
- case *client.UnexpectedHTTPResponseError: |
|
| 206 |
- return true |
|
| 207 |
- case error: |
|
| 208 |
- return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) |
|
| 209 |
- } |
|
| 210 |
- // let's be nice and fallback if the error is a completely |
|
| 211 |
- // unexpected one. |
|
| 212 |
- // If new errors have to be handled in some way, please |
|
| 213 |
- // add them to the switch above. |
|
| 214 |
- return true |
|
| 215 |
-} |
|
| 216 |
- |
|
| 217 | 172 |
// NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the |
| 218 | 173 |
// default TLS configuration. |
| 219 | 174 |
func NewTransport(tlsConfig *tls.Config) *http.Transport {
|