ebcb7d6b40
Use strongly typed errors to set HTTP status codes. Error interfaces are defined in the api/errors package and errors returned from controllers are checked against these interfaces. Errors can be wraeped in a pkg/errors.Causer, as long as somewhere in the line of causes one of the interfaces is implemented. The special error interfaces take precedence over Causer, meaning if both Causer and one of the new error interfaces are implemented, the Causer is not traversed. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
215 lines
5.8 KiB
Go
215 lines
5.8 KiB
Go
package distribution
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/docker/distribution/registry/api/errcode"
|
|
"github.com/docker/distribution/registry/api/v2"
|
|
"github.com/docker/distribution/registry/client"
|
|
"github.com/docker/distribution/registry/client/auth"
|
|
"github.com/docker/docker/distribution/xfer"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ErrNoSupport is an error type used for errors indicating that an operation
|
|
// is not supported. It encapsulates a more specific error.
|
|
type ErrNoSupport struct{ Err error }
|
|
|
|
func (e ErrNoSupport) Error() string {
|
|
if e.Err == nil {
|
|
return "not supported"
|
|
}
|
|
return e.Err.Error()
|
|
}
|
|
|
|
// fallbackError wraps an error that can possibly allow fallback to a different
|
|
// endpoint.
|
|
type fallbackError struct {
|
|
// err is the error being wrapped.
|
|
err error
|
|
// confirmedV2 is set to true if it was confirmed that the registry
|
|
// supports the v2 protocol. This is used to limit fallbacks to the v1
|
|
// protocol.
|
|
confirmedV2 bool
|
|
// transportOK is set to true if we managed to speak HTTP with the
|
|
// registry. This confirms that we're using appropriate TLS settings
|
|
// (or lack of TLS).
|
|
transportOK bool
|
|
}
|
|
|
|
// Error renders the FallbackError as a string.
|
|
func (f fallbackError) Error() string {
|
|
return f.Cause().Error()
|
|
}
|
|
|
|
func (f fallbackError) Cause() error {
|
|
return f.err
|
|
}
|
|
|
|
// shouldV2Fallback returns true if this error is a reason to fall back to v1.
|
|
func shouldV2Fallback(err errcode.Error) bool {
|
|
switch err.Code {
|
|
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
type notFoundError struct {
|
|
cause errcode.Error
|
|
ref reference.Named
|
|
}
|
|
|
|
func (e notFoundError) Error() string {
|
|
switch e.cause.Code {
|
|
case errcode.ErrorCodeDenied:
|
|
// ErrorCodeDenied is used when access to the repository was denied
|
|
return fmt.Sprintf("pull access denied for %s, repository does not exist or may require 'docker login'", reference.FamiliarName(e.ref))
|
|
case v2.ErrorCodeManifestUnknown:
|
|
return fmt.Sprintf("manifest for %s not found", reference.FamiliarString(e.ref))
|
|
case v2.ErrorCodeNameUnknown:
|
|
return fmt.Sprintf("repository %s not found", reference.FamiliarName(e.ref))
|
|
}
|
|
// Shouldn't get here, but this is better than returning an empty string
|
|
return e.cause.Message
|
|
}
|
|
|
|
func (e notFoundError) NotFound() {}
|
|
|
|
func (e notFoundError) Cause() error {
|
|
return e.cause
|
|
}
|
|
|
|
type unknownError struct {
|
|
cause error
|
|
}
|
|
|
|
func (e unknownError) Error() string {
|
|
return e.cause.Error()
|
|
}
|
|
|
|
func (e unknownError) Cause() error {
|
|
return e.cause
|
|
}
|
|
|
|
func (e unknownError) Unknown() {}
|
|
|
|
// TranslatePullError is used to convert an error from a registry pull
|
|
// operation to an error representing the entire pull operation. Any error
|
|
// information which is not used by the returned error gets output to
|
|
// log at info level.
|
|
func TranslatePullError(err error, ref reference.Named) error {
|
|
switch v := err.(type) {
|
|
case errcode.Errors:
|
|
if len(v) != 0 {
|
|
for _, extra := range v[1:] {
|
|
logrus.Infof("Ignoring extra error returned from registry: %v", extra)
|
|
}
|
|
return TranslatePullError(v[0], ref)
|
|
}
|
|
case errcode.Error:
|
|
switch v.Code {
|
|
case errcode.ErrorCodeDenied, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
|
|
return notFoundError{v, ref}
|
|
}
|
|
case xfer.DoNotRetry:
|
|
return TranslatePullError(v.Err, ref)
|
|
}
|
|
|
|
return unknownError{err}
|
|
}
|
|
|
|
// continueOnError returns true if we should fallback to the next endpoint
|
|
// as a result of this error.
|
|
func continueOnError(err error) bool {
|
|
switch v := err.(type) {
|
|
case errcode.Errors:
|
|
if len(v) == 0 {
|
|
return true
|
|
}
|
|
return continueOnError(v[0])
|
|
case ErrNoSupport:
|
|
return continueOnError(v.Err)
|
|
case errcode.Error:
|
|
return shouldV2Fallback(v)
|
|
case *client.UnexpectedHTTPResponseError:
|
|
return true
|
|
case ImageConfigPullError:
|
|
return false
|
|
case error:
|
|
return !strings.Contains(err.Error(), strings.ToLower(syscall.ESRCH.Error()))
|
|
}
|
|
// let's be nice and fallback if the error is a completely
|
|
// unexpected one.
|
|
// If new errors have to be handled in some way, please
|
|
// add them to the switch above.
|
|
return true
|
|
}
|
|
|
|
// retryOnError wraps the error in xfer.DoNotRetry if we should not retry the
|
|
// operation after this error.
|
|
func retryOnError(err error) error {
|
|
switch v := err.(type) {
|
|
case errcode.Errors:
|
|
if len(v) != 0 {
|
|
return retryOnError(v[0])
|
|
}
|
|
case errcode.Error:
|
|
switch v.Code {
|
|
case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied, errcode.ErrorCodeTooManyRequests, v2.ErrorCodeNameUnknown:
|
|
return xfer.DoNotRetry{Err: err}
|
|
}
|
|
case *url.Error:
|
|
switch v.Err {
|
|
case auth.ErrNoBasicAuthCredentials, auth.ErrNoToken:
|
|
return xfer.DoNotRetry{Err: v.Err}
|
|
}
|
|
return retryOnError(v.Err)
|
|
case *client.UnexpectedHTTPResponseError:
|
|
return xfer.DoNotRetry{Err: err}
|
|
case error:
|
|
if err == distribution.ErrBlobUnknown {
|
|
return xfer.DoNotRetry{Err: err}
|
|
}
|
|
if strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) {
|
|
return xfer.DoNotRetry{Err: err}
|
|
}
|
|
}
|
|
// let's be nice and fallback if the error is a completely
|
|
// unexpected one.
|
|
// If new errors have to be handled in some way, please
|
|
// add them to the switch above.
|
|
return err
|
|
}
|
|
|
|
type invalidManifestClassError struct {
|
|
mediaType string
|
|
class string
|
|
}
|
|
|
|
func (e invalidManifestClassError) Error() string {
|
|
return fmt.Sprintf("Encountered remote %q(%s) when fetching", e.mediaType, e.class)
|
|
}
|
|
|
|
func (e invalidManifestClassError) InvalidParameter() {}
|
|
|
|
type invalidManifestFormatError struct{}
|
|
|
|
func (invalidManifestFormatError) Error() string {
|
|
return "unsupported manifest format"
|
|
}
|
|
|
|
func (invalidManifestFormatError) InvalidParameter() {}
|
|
|
|
type reservedNameError string
|
|
|
|
func (e reservedNameError) Error() string {
|
|
return "'" + string(e) + "' is a reserved name"
|
|
}
|
|
|
|
func (e reservedNameError) Forbidden() {}
|