Remove string checking in API error handling

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>
Upstream-commit: ebcb7d6b406fe50ea9a237c73004d75884184c33
Component: engine
This commit is contained in:
Brian Goff
2017-08-15 16:01:11 -04:00
parent 7eea9c9616
commit 30f1b651e2
127 changed files with 1791 additions and 885 deletions
+54
View File
@@ -0,0 +1,54 @@
package errdefs
// ErrNotFound signals that the requested object doesn't exist
type ErrNotFound interface {
NotFound()
}
// ErrInvalidParameter signals that the user input is invalid
type ErrInvalidParameter interface {
InvalidParameter()
}
// ErrConflict signals that some internal state conflicts with the requested action and can't be performed.
// A change in state should be able to clear this error.
type ErrConflict interface {
Conflict()
}
// ErrUnauthorized is used to signify that the user is not authorized to perform a specific action
type ErrUnauthorized interface {
Unauthorized()
}
// ErrUnavailable signals that the requested action/subsystem is not available.
type ErrUnavailable interface {
Unavailable()
}
// ErrForbidden signals that the requested action cannot be performed under any circumstances.
// When a ErrForbidden is returned, the caller should never retry the action.
type ErrForbidden interface {
Forbidden()
}
// ErrSystem signals that some internal error occurred.
// An example of this would be a failed mount request.
type ErrSystem interface {
ErrSystem()
}
// ErrNotModified signals that an action can't be performed because it's already in the desired state
type ErrNotModified interface {
NotModified()
}
// ErrNotImplemented signals that the requested action/feature is not implemented on the system as configured.
type ErrNotImplemented interface {
NotImplemented()
}
// ErrUnknown signals that the kind of error that occurred is not known.
type ErrUnknown interface {
Unknown()
}
+8
View File
@@ -0,0 +1,8 @@
// Package errdefs defines a set of error interfaces that packages should use for communicating classes of errors.
// Errors that cross the package boundary should implement one (and only one) of these interfaces.
//
// Packages should not reference these interfaces directly, only implement them.
// To check if a particular error implements one of these interfaces, there are helper
// functions provided (e.g. `Is<SomeError>`) which can be used rather than asserting the interfaces directly.
// If you must assert on these interfaces, be sure to check the causal chain (`err.Cause()`).
package errdefs
+86
View File
@@ -0,0 +1,86 @@
package errdefs
type causer interface {
Cause() error
}
func getImplementer(err error) error {
switch e := err.(type) {
case
ErrNotFound,
ErrInvalidParameter,
ErrConflict,
ErrUnauthorized,
ErrUnavailable,
ErrForbidden,
ErrSystem,
ErrNotModified,
ErrNotImplemented,
ErrUnknown:
return e
case causer:
return getImplementer(e.Cause())
default:
return err
}
}
// IsNotFound returns if the passed in error is a ErrNotFound
func IsNotFound(err error) bool {
_, ok := getImplementer(err).(ErrNotFound)
return ok
}
// IsInvalidParameter returns if the passed in error is an ErrInvalidParameter
func IsInvalidParameter(err error) bool {
_, ok := getImplementer(err).(ErrInvalidParameter)
return ok
}
// IsConflict returns if the passed in error is a ErrConflict
func IsConflict(err error) bool {
_, ok := getImplementer(err).(ErrConflict)
return ok
}
// IsUnauthorized returns if the the passed in error is an ErrUnauthorized
func IsUnauthorized(err error) bool {
_, ok := getImplementer(err).(ErrUnauthorized)
return ok
}
// IsUnavailable returns if the passed in error is an ErrUnavailable
func IsUnavailable(err error) bool {
_, ok := getImplementer(err).(ErrUnavailable)
return ok
}
// IsForbidden returns if the passed in error is a ErrForbidden
func IsForbidden(err error) bool {
_, ok := getImplementer(err).(ErrForbidden)
return ok
}
// IsSystem returns if the passed in error is a ErrSystem
func IsSystem(err error) bool {
_, ok := getImplementer(err).(ErrSystem)
return ok
}
// IsNotModified returns if the passed in error is a NotModified error
func IsNotModified(err error) bool {
_, ok := getImplementer(err).(ErrNotModified)
return ok
}
// IsNotImplemented returns if the passed in error is a ErrNotImplemented
func IsNotImplemented(err error) bool {
_, ok := getImplementer(err).(ErrNotImplemented)
return ok
}
// IsUnknown returns if the passed in error is an ErrUnknown
func IsUnknown(err error) bool {
_, ok := getImplementer(err).(ErrUnknown)
return ok
}
-47
View File
@@ -1,47 +0,0 @@
package errors
import "net/http"
// apiError is an error wrapper that also
// holds information about response status codes.
type apiError struct {
error
statusCode int
}
// HTTPErrorStatusCode returns a status code.
func (e apiError) HTTPErrorStatusCode() int {
return e.statusCode
}
// NewErrorWithStatusCode allows you to associate
// a specific HTTP Status Code to an error.
// The server will take that code and set
// it as the response status.
func NewErrorWithStatusCode(err error, code int) error {
return apiError{err, code}
}
// NewBadRequestError creates a new API error
// that has the 400 HTTP status code associated to it.
func NewBadRequestError(err error) error {
return NewErrorWithStatusCode(err, http.StatusBadRequest)
}
// NewRequestForbiddenError creates a new API error
// that has the 403 HTTP status code associated to it.
func NewRequestForbiddenError(err error) error {
return NewErrorWithStatusCode(err, http.StatusForbidden)
}
// NewRequestNotFoundError creates a new API error
// that has the 404 HTTP status code associated to it.
func NewRequestNotFoundError(err error) error {
return NewErrorWithStatusCode(err, http.StatusNotFound)
}
// NewRequestConflictError creates a new API error
// that has the 409 HTTP status code associated to it.
func NewRequestConflictError(err error) error {
return NewErrorWithStatusCode(err, http.StatusConflict)
}
@@ -1,64 +0,0 @@
package errors
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
func newError(errorname string) error {
return fmt.Errorf("test%v", errorname)
}
func TestErrors(t *testing.T) {
errmsg := newError("apiError")
err := apiError{
error: errmsg,
statusCode: 0,
}
assert.Equal(t, err.HTTPErrorStatusCode(), err.statusCode)
errmsg = newError("ErrorWithStatusCode")
errcode := 1
serr := NewErrorWithStatusCode(errmsg, errcode)
apierr, ok := serr.(apiError)
if !ok {
t.Fatal("excepted err is apiError type")
}
assert.Equal(t, errcode, apierr.statusCode)
errmsg = newError("NewBadRequestError")
baderr := NewBadRequestError(errmsg)
apierr, ok = baderr.(apiError)
if !ok {
t.Fatal("excepted err is apiError type")
}
assert.Equal(t, http.StatusBadRequest, apierr.statusCode)
errmsg = newError("RequestForbiddenError")
ferr := NewRequestForbiddenError(errmsg)
apierr, ok = ferr.(apiError)
if !ok {
t.Fatal("excepted err is apiError type")
}
assert.Equal(t, http.StatusForbidden, apierr.statusCode)
errmsg = newError("RequestNotFoundError")
nerr := NewRequestNotFoundError(errmsg)
apierr, ok = nerr.(apiError)
if !ok {
t.Fatal("excepted err is apiError type")
}
assert.Equal(t, http.StatusNotFound, apierr.statusCode)
errmsg = newError("RequestConflictError")
cerr := NewRequestConflictError(errmsg)
apierr, ok = cerr.(apiError)
if !ok {
t.Fatal("excepted err is apiError type")
}
assert.Equal(t, http.StatusConflict, apierr.statusCode)
}
@@ -1,9 +1,10 @@
package httputils
import (
"fmt"
"net/http"
"strings"
"github.com/docker/docker/api/errdefs"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/gorilla/mux"
@@ -20,13 +21,8 @@ type httpStatusError interface {
HTTPErrorStatusCode() int
}
// inputValidationError is an interface
// that errors generated by invalid
// inputs can implement to tell the
// api layer to set a 400 status code
// in the response.
type inputValidationError interface {
IsValidationError() bool
type causer interface {
Cause() error
}
// GetHTTPErrorStatusCode retrieves status code from error message.
@@ -37,49 +33,44 @@ func GetHTTPErrorStatusCode(err error) int {
}
var statusCode int
errMsg := err.Error()
switch e := err.(type) {
case httpStatusError:
statusCode = e.HTTPErrorStatusCode()
case inputValidationError:
// Stop right there
// Are you sure you should be adding a new error class here? Do one of the existing ones work?
// Note that the below functions are already checking the error causal chain for matches.
switch {
case errdefs.IsNotFound(err):
statusCode = http.StatusNotFound
case errdefs.IsInvalidParameter(err):
statusCode = http.StatusBadRequest
case errdefs.IsConflict(err):
statusCode = http.StatusConflict
case errdefs.IsUnauthorized(err):
statusCode = http.StatusUnauthorized
case errdefs.IsUnavailable(err):
statusCode = http.StatusServiceUnavailable
case errdefs.IsForbidden(err):
statusCode = http.StatusForbidden
case errdefs.IsNotModified(err):
statusCode = http.StatusNotModified
case errdefs.IsNotImplemented(err):
statusCode = http.StatusNotImplemented
case errdefs.IsSystem(err) || errdefs.IsUnknown(err):
statusCode = http.StatusInternalServerError
default:
statusCode = statusCodeFromGRPCError(err)
if statusCode != http.StatusInternalServerError {
return statusCode
}
// FIXME: this is brittle and should not be necessary, but we still need to identify if
// there are errors falling back into this logic.
// If we need to differentiate between different possible error types,
// we should create appropriate error types that implement the httpStatusError interface.
errStr := strings.ToLower(errMsg)
for _, status := range []struct {
keyword string
code int
}{
{"not found", http.StatusNotFound},
{"cannot find", http.StatusNotFound},
{"no such", http.StatusNotFound},
{"bad parameter", http.StatusBadRequest},
{"no command", http.StatusBadRequest},
{"conflict", http.StatusConflict},
{"impossible", http.StatusNotAcceptable},
{"wrong login/password", http.StatusUnauthorized},
{"unauthorized", http.StatusUnauthorized},
{"hasn't been activated", http.StatusForbidden},
{"this node", http.StatusServiceUnavailable},
{"needs to be unlocked", http.StatusServiceUnavailable},
{"certificates have expired", http.StatusServiceUnavailable},
{"repository does not exist", http.StatusNotFound},
} {
if strings.Contains(errStr, status.keyword) {
statusCode = status.code
break
}
if e, ok := err.(causer); ok {
return GetHTTPErrorStatusCode(e.Cause())
}
logrus.WithFields(logrus.Fields{
"module": "api",
"error_type": fmt.Sprintf("%T", err),
}).Debugf("FIXME: Got an API for which error does not match any expected type!!!: %+v", err)
}
if statusCode == 0 {
@@ -133,6 +124,9 @@ func statusCodeFromGRPCError(err error) int {
case codes.Unavailable: // code 14
return http.StatusServiceUnavailable
default:
if e, ok := err.(causer); ok {
return statusCodeFromGRPCError(e.Cause())
}
// codes.Canceled(1)
// codes.Unknown(2)
// codes.DeadlineExceeded(4)
+16 -8
View File
@@ -1,7 +1,6 @@
package httputils
import (
"errors"
"net/http"
"path/filepath"
"strconv"
@@ -49,6 +48,16 @@ type ArchiveOptions struct {
Path string
}
type badParameterError struct {
param string
}
func (e badParameterError) Error() string {
return "bad parameter: " + e.param + "cannot be empty"
}
func (e badParameterError) InvalidParameter() {}
// ArchiveFormValues parses form values and turns them into ArchiveOptions.
// It fails if the archive name and path are not in the request.
func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions, error) {
@@ -57,14 +66,13 @@ func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions,
}
name := vars["name"]
path := filepath.FromSlash(r.Form.Get("path"))
switch {
case name == "":
return ArchiveOptions{}, errors.New("bad parameter: 'name' cannot be empty")
case path == "":
return ArchiveOptions{}, errors.New("bad parameter: 'path' cannot be empty")
if name == "" {
return ArchiveOptions{}, badParameterError{"name"}
}
path := filepath.FromSlash(r.Form.Get("path"))
if path == "" {
return ArchiveOptions{}, badParameterError{"path"}
}
return ArchiveOptions{name, path}, nil
}
@@ -1,12 +1,12 @@
package httputils
import (
"fmt"
"io"
"mime"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@@ -43,6 +43,20 @@ func CloseStreams(streams ...interface{}) {
}
}
type validationError struct {
cause error
}
func (e validationError) Error() string {
return e.cause.Error()
}
func (e validationError) Cause() error {
return e.cause
}
func (e validationError) InvalidParameter() {}
// CheckForJSON makes sure that the request's Content-Type is application/json.
func CheckForJSON(r *http.Request) error {
ct := r.Header.Get("Content-Type")
@@ -58,7 +72,7 @@ func CheckForJSON(r *http.Request) error {
if matchesContentType(ct, "application/json") {
return nil
}
return fmt.Errorf("Content-Type specified (%s) must be 'application/json'", ct)
return validationError{errors.Errorf("Content-Type specified (%s) must be 'application/json'", ct)}
}
// ParseForm ensures the request form is parsed even with invalid content types.
@@ -68,7 +82,7 @@ func ParseForm(r *http.Request) error {
return nil
}
if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
return err
return validationError{err}
}
return nil
}
@@ -5,7 +5,6 @@ import (
"net/http"
"runtime"
"github.com/docker/docker/api/errors"
"github.com/docker/docker/api/types/versions"
"golang.org/x/net/context"
)
@@ -28,6 +27,16 @@ func NewVersionMiddleware(s, d, m string) VersionMiddleware {
}
}
type versionUnsupportedError struct {
version, minVersion string
}
func (e versionUnsupportedError) Error() string {
return fmt.Sprintf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", e.version, e.minVersion)
}
func (e versionUnsupportedError) InvalidParameter() {}
// WrapHandler returns a new handler function wrapping the previous one in the request chain.
func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
@@ -37,7 +46,7 @@ func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.
}
if versions.LessThan(apiVersion, v.minVersion) {
return errors.NewBadRequestError(fmt.Errorf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", apiVersion, v.minVersion))
return versionUnsupportedError{apiVersion, v.minVersion}
}
header := fmt.Sprintf("Docker/%s (%s)", v.serverVersion, runtime.GOOS)
@@ -12,7 +12,6 @@ import (
"strings"
"sync"
apierrors "github.com/docker/docker/api/errors"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
@@ -27,6 +26,14 @@ import (
"golang.org/x/net/context"
)
type invalidIsolationError string
func (e invalidIsolationError) Error() string {
return fmt.Sprintf("Unsupported isolation: %q", string(e))
}
func (e invalidIsolationError) InvalidParameter() {}
func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) {
version := httputils.VersionFromContext(ctx)
options := &types.ImageBuildOptions{}
@@ -71,20 +78,20 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
if i := container.Isolation(r.FormValue("isolation")); i != "" {
if !container.Isolation.IsValid(i) {
return nil, fmt.Errorf("Unsupported isolation: %q", i)
return nil, invalidIsolationError(i)
}
options.Isolation = i
}
if runtime.GOOS != "windows" && options.SecurityOpt != nil {
return nil, fmt.Errorf("The daemon on this platform does not support setting security options on build")
return nil, validationError{fmt.Errorf("The daemon on this platform does not support setting security options on build")}
}
var buildUlimits = []*units.Ulimit{}
ulimitsJSON := r.FormValue("ulimits")
if ulimitsJSON != "" {
if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
return nil, err
return nil, errors.Wrap(validationError{err}, "error reading ulimit settings")
}
options.Ulimits = buildUlimits
}
@@ -105,7 +112,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
if buildArgsJSON != "" {
var buildArgs = map[string]*string{}
if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
return nil, err
return nil, errors.Wrap(validationError{err}, "error reading build args")
}
options.BuildArgs = buildArgs
}
@@ -114,7 +121,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
if labelsJSON != "" {
var labels = map[string]string{}
if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
return nil, err
return nil, errors.Wrap(validationError{err}, "error reading labels")
}
options.Labels = labels
}
@@ -140,6 +147,16 @@ func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *
return httputils.WriteJSON(w, http.StatusOK, report)
}
type validationError struct {
cause error
}
func (e validationError) Error() string {
return e.cause.Error()
}
func (e validationError) InvalidParameter() {}
func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var (
notVerboseBuffer = bytes.NewBuffer(nil)
@@ -173,8 +190,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
buildOptions.AuthConfigs = getAuthConfigs(r.Header)
if buildOptions.Squash && !br.daemon.HasExperimental() {
return apierrors.NewBadRequestError(
errors.New("squash is only supported with experimental mode"))
return validationError{errors.New("squash is only supported with experimental mode")}
}
out := io.Writer(output)
@@ -51,7 +51,7 @@ type stateBackend interface {
type monitorBackend interface {
ContainerChanges(name string) ([]archive.Change, error)
ContainerInspect(name string, size bool, version string) (interface{}, error)
ContainerLogs(ctx context.Context, name string, config *types.ContainerLogsOptions) (<-chan *backend.LogMessage, error)
ContainerLogs(ctx context.Context, name string, config *types.ContainerLogsOptions) (msgs <-chan *backend.LogMessage, tty bool, err error)
ContainerStats(ctx context.Context, name string, config *backend.ContainerStatsConfig) error
ContainerTop(name string, psArgs string) (*container.ContainerTopOKBody, error)
@@ -6,13 +6,19 @@ import (
)
type validationError struct {
error
cause error
}
func (validationError) IsValidationError() bool {
return true
func (e validationError) Error() string {
return e.cause.Error()
}
func (e validationError) Cause() error {
return e.cause
}
func (e validationError) InvalidParameter() {}
// containerRouter is a router to talk with the container controller
type containerRouter struct {
backend Backend
@@ -8,7 +8,7 @@ import (
"strconv"
"syscall"
"github.com/docker/docker/api"
"github.com/docker/docker/api/errdefs"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
@@ -18,6 +18,7 @@ import (
containerpkg "github.com/docker/docker/container"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/signal"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
"golang.org/x/net/websocket"
@@ -87,7 +88,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
// with the appropriate status code.
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
if !(stdout || stderr) {
return fmt.Errorf("Bad parameters: you must choose at least one stream")
return validationError{errors.New("Bad parameters: you must choose at least one stream")}
}
containerName := vars["name"]
@@ -101,19 +102,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
Details: httputils.BoolValue(r, "details"),
}
// doesn't matter what version the client is on, we're using this internally only
// also do we need size? i'm thinking no we don't
raw, err := s.backend.ContainerInspect(containerName, false, api.DefaultVersion)
if err != nil {
return err
}
container, ok := raw.(*types.ContainerJSON)
if !ok {
// %T prints the type. handy!
return fmt.Errorf("expected container to be *types.ContainerJSON but got %T", raw)
}
msgs, err := s.backend.ContainerLogs(ctx, containerName, logsConfig)
msgs, tty, err := s.backend.ContainerLogs(ctx, containerName, logsConfig)
if err != nil {
return err
}
@@ -122,7 +111,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
// this is the point of no return for writing a response. once we call
// WriteLogStream, the response has been started and errors will be
// returned in band by WriteLogStream
httputils.WriteLogStream(ctx, w, msgs, logsConfig, !container.Config.Tty)
httputils.WriteLogStream(ctx, w, msgs, logsConfig, !tty)
return nil
}
@@ -130,6 +119,14 @@ func (s *containerRouter) getContainersExport(ctx context.Context, w http.Respon
return s.backend.ContainerExport(vars["name"], w)
}
type bodyOnStartError struct{}
func (bodyOnStartError) Error() string {
return "starting container with non-empty request body was deprecated since API v1.22 and removed in v1.24"
}
func (bodyOnStartError) InvalidParameter() {}
func (s *containerRouter) postContainersStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
// If contentLength is -1, we can assumed chunked encoding
// or more technically that the length is unknown
@@ -143,7 +140,7 @@ func (s *containerRouter) postContainersStart(ctx context.Context, w http.Respon
// A non-nil json object is at least 7 characters.
if r.ContentLength > 7 || r.ContentLength == -1 {
if versions.GreaterThanOrEqualTo(version, "1.24") {
return validationError{fmt.Errorf("starting container with non-empty request body was deprecated since v1.10 and removed in v1.12")}
return bodyOnStartError{}
}
if err := httputils.CheckForJSON(r); err != nil {
@@ -193,10 +190,6 @@ func (s *containerRouter) postContainersStop(ctx context.Context, w http.Respons
return nil
}
type errContainerIsRunning interface {
ContainerIsRunning() bool
}
func (s *containerRouter) postContainersKill(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
@@ -209,14 +202,14 @@ func (s *containerRouter) postContainersKill(ctx context.Context, w http.Respons
if sigStr := r.Form.Get("signal"); sigStr != "" {
var err error
if sig, err = signal.ParseSignal(sigStr); err != nil {
return err
return validationError{err}
}
}
if err := s.backend.ContainerKill(name, uint64(sig)); err != nil {
var isStopped bool
if e, ok := err.(errContainerIsRunning); ok {
isStopped = !e.ContainerIsRunning()
if errdefs.IsConflict(err) {
isStopped = true
}
// Return error that's not caused because the container is stopped.
@@ -224,7 +217,7 @@ func (s *containerRouter) postContainersKill(ctx context.Context, w http.Respons
// to keep backwards compatibility.
version := httputils.VersionFromContext(ctx)
if versions.GreaterThanOrEqualTo(version, "1.20") || !isStopped {
return fmt.Errorf("Cannot kill container %s: %v", name, err)
return errors.Wrapf(err, "Cannot kill container: %s", name)
}
}
@@ -458,11 +451,11 @@ func (s *containerRouter) postContainersResize(ctx context.Context, w http.Respo
height, err := strconv.Atoi(r.Form.Get("h"))
if err != nil {
return err
return validationError{err}
}
width, err := strconv.Atoi(r.Form.Get("w"))
if err != nil {
return err
return validationError{err}
}
return s.backend.ContainerResize(vars["name"], height, width)
@@ -480,7 +473,7 @@ func (s *containerRouter) postContainersAttach(ctx context.Context, w http.Respo
hijacker, ok := w.(http.Hijacker)
if !ok {
return fmt.Errorf("error attaching to container %s, hijack connection missing", containerName)
return validationError{errors.Errorf("error attaching to container %s, hijack connection missing", containerName)}
}
setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) {
@@ -597,7 +590,7 @@ func (s *containerRouter) postContainersPrune(ctx context.Context, w http.Respon
pruneFilters, err := filters.FromParam(r.Form.Get("filters"))
if err != nil {
return err
return validationError{err}
}
pruneReport, err := s.backend.ContainersPrune(ctx, pruneFilters)
@@ -3,11 +3,8 @@ package container
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/types"
@@ -15,6 +12,14 @@ import (
"golang.org/x/net/context"
)
type pathError struct{}
func (pathError) Error() string {
return "Path cannot be empty"
}
func (pathError) InvalidParameter() {}
// postContainersCopy is deprecated in favor of getContainersArchive.
func (s *containerRouter) postContainersCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
// Deprecated since 1.8, Errors out since 1.12
@@ -33,18 +38,11 @@ func (s *containerRouter) postContainersCopy(ctx context.Context, w http.Respons
}
if cfg.Resource == "" {
return fmt.Errorf("Path cannot be empty")
return pathError{}
}
data, err := s.backend.ContainerCopy(vars["name"], cfg.Resource)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "no such container") {
w.WriteHeader(http.StatusNotFound)
return nil
}
if os.IsNotExist(err) {
return fmt.Errorf("Could not find the file %s in container %s", cfg.Resource, vars["name"])
}
return err
}
defer data.Close()
@@ -24,6 +24,14 @@ func (s *containerRouter) getExecByID(ctx context.Context, w http.ResponseWriter
return httputils.WriteJSON(w, http.StatusOK, eConfig)
}
type execCommandError struct{}
func (execCommandError) Error() string {
return "No exec command specified"
}
func (execCommandError) InvalidParameter() {}
func (s *containerRouter) postContainerExecCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
@@ -39,7 +47,7 @@ func (s *containerRouter) postContainerExecCreate(ctx context.Context, w http.Re
}
if len(execConfig.Cmd) == 0 {
return fmt.Errorf("No exec command specified")
return execCommandError{}
}
// Register an instance of Exec in container.
@@ -129,11 +137,11 @@ func (s *containerRouter) postContainerExecResize(ctx context.Context, w http.Re
}
height, err := strconv.Atoi(r.Form.Get("h"))
if err != nil {
return err
return validationError{err}
}
width, err := strconv.Atoi(r.Form.Get("w"))
if err != nil {
return err
return validationError{err}
}
return s.backend.ContainerExecResize(vars["name"], height, width)
@@ -1,19 +1,13 @@
package router
import (
"errors"
"net/http"
"golang.org/x/net/context"
apierrors "github.com/docker/docker/api/errors"
"github.com/docker/docker/api/server/httputils"
)
var (
errExperimentalFeature = errors.New("This experimental feature is disabled by default. Start the Docker daemon in experimental mode in order to enable it.")
)
// ExperimentalRoute defines an experimental API route that can be enabled or disabled.
type ExperimentalRoute interface {
Route
@@ -39,8 +33,16 @@ func (r *experimentalRoute) Disable() {
r.handler = experimentalHandler
}
type notImplementedError struct{}
func (notImplementedError) Error() string {
return "This experimental feature is disabled by default. Start the Docker daemon in experimental mode in order to enable it."
}
func (notImplementedError) NotImplemented() {}
func experimentalHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
return apierrors.NewErrorWithStatusCode(errExperimentalFeature, http.StatusNotImplemented)
return notImplementedError{}
}
// Handler returns returns the APIFunc to let the server wrap it in middlewares.
@@ -3,7 +3,6 @@ package image
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
@@ -20,6 +19,7 @@ import (
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
@@ -161,6 +161,20 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite
return nil
}
type validationError struct {
cause error
}
func (e validationError) Error() string {
return e.cause.Error()
}
func (e validationError) Cause() error {
return e.cause
}
func (validationError) InvalidParameter() {}
func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
metaHeaders := map[string][]string{}
for k, v := range r.Header {
@@ -184,7 +198,7 @@ func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter,
} else {
// the old format is supported for compatibility if there was no authConfig header
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
return fmt.Errorf("Bad parameters and missing X-Registry-Auth: %v", err)
return errors.Wrap(validationError{err}, "Bad parameters and missing X-Registry-Auth")
}
}
@@ -246,6 +260,14 @@ func (s *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter,
return nil
}
type missingImageError struct{}
func (missingImageError) Error() string {
return "image name cannot be blank"
}
func (missingImageError) InvalidParameter() {}
func (s *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
@@ -254,7 +276,7 @@ func (s *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r
name := vars["name"]
if strings.TrimSpace(name) == "" {
return fmt.Errorf("image name cannot be blank")
return missingImageError{}
}
force := httputils.BoolValue(r, "force")
@@ -1,8 +1,6 @@
package network
import (
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/runconfig"
@@ -24,11 +22,19 @@ func filterNetworkByType(nws []types.NetworkResource, netType string) ([]types.N
}
}
default:
return nil, fmt.Errorf("Invalid filter: 'type'='%s'", netType)
return nil, invalidFilter(netType)
}
return retNws, nil
}
type invalidFilter string
func (e invalidFilter) Error() string {
return "Invalid filter: 'type'='" + string(e) + "'"
}
func (e invalidFilter) InvalidParameter() {}
// filterNetworks filters network list according to user specified filter
// and returns user chosen networks
func filterNetworks(nws []types.NetworkResource, filter filters.Args) ([]types.NetworkResource, error) {
@@ -2,14 +2,12 @@ package network
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"golang.org/x/net/context"
"github.com/docker/docker/api/errors"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@@ -18,6 +16,7 @@ import (
"github.com/docker/libnetwork"
netconst "github.com/docker/libnetwork/datastore"
"github.com/docker/libnetwork/networkdb"
"github.com/pkg/errors"
)
var (
@@ -83,6 +82,24 @@ SKIP:
return httputils.WriteJSON(w, http.StatusOK, list)
}
type invalidRequestError struct {
cause error
}
func (e invalidRequestError) Error() string {
return e.cause.Error()
}
func (e invalidRequestError) InvalidParameter() {}
type ambigousResultsError string
func (e ambigousResultsError) Error() string {
return "network " + string(e) + " is ambiguous"
}
func (ambigousResultsError) InvalidParameter() {}
func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
@@ -95,8 +112,7 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
)
if v := r.URL.Query().Get("verbose"); v != "" {
if verbose, err = strconv.ParseBool(v); err != nil {
err = fmt.Errorf("invalid value for verbose: %s", v)
return errors.NewBadRequestError(err)
return errors.Wrapf(invalidRequestError{err}, "invalid value for verbose: %s", v)
}
}
scope := r.URL.Query().Get("scope")
@@ -177,7 +193,7 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
}
}
if len(listByFullName) > 1 {
return fmt.Errorf("network %s is ambiguous (%d matches found based on name)", term, len(listByFullName))
return errors.Wrapf(ambigousResultsError(term), "%d matches found based on name", len(listByFullName))
}
// Find based on partial ID, returns true only if no duplicates
@@ -187,7 +203,7 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
}
}
if len(listByPartialID) > 1 {
return fmt.Errorf("network %s is ambiguous (%d matches found based on ID prefix)", term, len(listByPartialID))
return errors.Wrapf(ambigousResultsError(term), "%d matches found based on ID prefix", len(listByPartialID))
}
return libnetwork.ErrNoSuchNetwork(term)
@@ -3,14 +3,27 @@ package session
import (
"net/http"
apierrors "github.com/docker/docker/api/errors"
"golang.org/x/net/context"
)
type invalidRequest struct {
cause error
}
func (e invalidRequest) Error() string {
return e.cause.Error()
}
func (e invalidRequest) Cause() error {
return e.cause
}
func (e invalidRequest) InvalidParameter() {}
func (sr *sessionRouter) startSession(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
err := sr.backend.HandleHTTPRequest(ctx, w, r)
if err != nil {
return apierrors.NewBadRequestError(err)
return invalidRequest{err}
}
return nil
}
@@ -6,13 +6,13 @@ import (
"net/http"
"strconv"
"github.com/docker/docker/api/errors"
"github.com/docker/docker/api/server/httputils"
basictypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/api/types/filters"
types "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@@ -57,6 +57,20 @@ func (sr *swarmRouter) inspectCluster(ctx context.Context, w http.ResponseWriter
return httputils.WriteJSON(w, http.StatusOK, swarm)
}
type invalidRequestError struct {
err error
}
func (e invalidRequestError) Error() string {
return e.err.Error()
}
func (e invalidRequestError) Cause() error {
return e.err
}
func (e invalidRequestError) InvalidParameter() {}
func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var swarm types.Spec
if err := json.NewDecoder(r.Body).Decode(&swarm); err != nil {
@@ -67,7 +81,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
version, err := strconv.ParseUint(rawVersion, 10, 64)
if err != nil {
err := fmt.Errorf("invalid swarm version '%s': %v", rawVersion, err)
return errors.NewBadRequestError(err)
return invalidRequestError{err}
}
var flags types.UpdateFlags
@@ -76,7 +90,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
rot, err := strconv.ParseBool(value)
if err != nil {
err := fmt.Errorf("invalid value for rotateWorkerToken: %s", value)
return errors.NewBadRequestError(err)
return invalidRequestError{err}
}
flags.RotateWorkerToken = rot
@@ -86,7 +100,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
rot, err := strconv.ParseBool(value)
if err != nil {
err := fmt.Errorf("invalid value for rotateManagerToken: %s", value)
return errors.NewBadRequestError(err)
return invalidRequestError{err}
}
flags.RotateManagerToken = rot
@@ -95,7 +109,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
if value := r.URL.Query().Get("rotateManagerUnlockKey"); value != "" {
rot, err := strconv.ParseBool(value)
if err != nil {
return errors.NewBadRequestError(fmt.Errorf("invalid value for rotateManagerUnlockKey: %s", value))
return invalidRequestError{fmt.Errorf("invalid value for rotateManagerUnlockKey: %s", value)}
}
flags.RotateManagerUnlockKey = rot
@@ -139,7 +153,7 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
}
filter, err := filters.FromParam(r.Form.Get("filters"))
if err != nil {
return err
return invalidRequestError{err}
}
services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filters: filter})
@@ -158,7 +172,7 @@ func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r
insertDefaults, err = strconv.ParseBool(value)
if err != nil {
err := fmt.Errorf("invalid value for insertDefaults: %s", value)
return errors.NewBadRequestError(err)
return errors.Wrapf(invalidRequestError{err}, "invalid value for insertDefaults: %s", value)
}
}
@@ -204,7 +218,7 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
version, err := strconv.ParseUint(rawVersion, 10, 64)
if err != nil {
err := fmt.Errorf("invalid service version '%s': %v", rawVersion, err)
return errors.NewBadRequestError(err)
return invalidRequestError{err}
}
var flags basictypes.ServiceUpdateOptions
@@ -297,7 +311,7 @@ func (sr *swarmRouter) updateNode(ctx context.Context, w http.ResponseWriter, r
version, err := strconv.ParseUint(rawVersion, 10, 64)
if err != nil {
err := fmt.Errorf("invalid node version '%s': %v", rawVersion, err)
return errors.NewBadRequestError(err)
return invalidRequestError{err}
}
if err := sr.backend.UpdateNode(vars["id"], version, node); err != nil {
@@ -403,13 +417,13 @@ func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *
func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var secret types.SecretSpec
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
return errors.NewBadRequestError(err)
return invalidRequestError{err}
}
rawVersion := r.URL.Query().Get("version")
version, err := strconv.ParseUint(rawVersion, 10, 64)
if err != nil {
return errors.NewBadRequestError(fmt.Errorf("invalid secret version"))
return invalidRequestError{fmt.Errorf("invalid secret version")}
}
id := vars["id"]
@@ -474,13 +488,13 @@ func (sr *swarmRouter) getConfig(ctx context.Context, w http.ResponseWriter, r *
func (sr *swarmRouter) updateConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var config types.ConfigSpec
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
return errors.NewBadRequestError(err)
return invalidRequestError{err}
}
rawVersion := r.URL.Query().Get("version")
version, err := strconv.ParseUint(rawVersion, 10, 64)
if err != nil {
return errors.NewBadRequestError(fmt.Errorf("invalid config version"))
return invalidRequestError{fmt.Errorf("invalid config version")}
}
id := vars["id"]
@@ -7,7 +7,6 @@ import (
"time"
"github.com/docker/docker/api"
"github.com/docker/docker/api/errors"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
@@ -85,6 +84,16 @@ func (s *systemRouter) getDiskUsage(ctx context.Context, w http.ResponseWriter,
return httputils.WriteJSON(w, http.StatusOK, du)
}
type invalidRequestError struct {
Err error
}
func (e invalidRequestError) Error() string {
return e.Err.Error()
}
func (e invalidRequestError) InvalidParameter() {}
func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
@@ -105,7 +114,7 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *
)
if !until.IsZero() {
if until.Before(since) {
return errors.NewBadRequestError(fmt.Errorf("`since` time (%s) cannot be after `until` time (%s)", r.Form.Get("since"), r.Form.Get("until")))
return invalidRequestError{fmt.Errorf("`since` time (%s) cannot be after `until` time (%s)", r.Form.Get("since"), r.Form.Get("until"))}
}
now := time.Now()
+9 -4
View File
@@ -2,12 +2,10 @@ package server
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"github.com/docker/docker/api/errors"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/server/middleware"
"github.com/docker/docker/api/server/router"
@@ -158,6 +156,14 @@ func (s *Server) InitRouter(routers ...router.Router) {
}
}
type pageNotFoundError struct{}
func (pageNotFoundError) Error() string {
return "page not found"
}
func (pageNotFoundError) NotFound() {}
// createMux initializes the main router the server uses.
func (s *Server) createMux() *mux.Router {
m := mux.NewRouter()
@@ -180,8 +186,7 @@ func (s *Server) createMux() *mux.Router {
m.Path("/debug" + r.Path()).Handler(f)
}
err := errors.NewRequestNotFoundError(fmt.Errorf("page not found"))
notFoundHandler := httputils.MakeErrorHandler(err)
notFoundHandler := httputils.MakeErrorHandler(pageNotFoundError{})
m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler)
m.NotFoundHandler = notFoundHandler
-4
View File
@@ -3311,10 +3311,6 @@ paths:
examples:
application/json:
message: "No such container: c2ada9df5af8"
406:
description: "impossible to attach"
schema:
$ref: "#/definitions/ErrorResponse"
409:
description: "conflict"
schema:
+17 -2
View File
@@ -5,7 +5,6 @@ package filters
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
@@ -258,17 +257,33 @@ func (filters Args) Include(field string) bool {
return ok
}
type invalidFilter string
func (e invalidFilter) Error() string {
return "Invalid filter '" + string(e) + "'"
}
func (invalidFilter) InvalidParameter() {}
// Validate ensures that all the fields in the filter are valid.
// It returns an error as soon as it finds an invalid field.
func (filters Args) Validate(accepted map[string]bool) error {
for name := range filters.fields {
if !accepted[name] {
return fmt.Errorf("Invalid filter '%s'", name)
return invalidFilter(name)
}
}
return nil
}
type invalidFilterError string
func (e invalidFilterError) Error() string {
return "Invalid filter: '" + string(e) + "'"
}
func (invalidFilterError) InvalidParameter() {}
// WalkValues iterates over the list of filtered values for a field.
// It stops the iteration if it finds an error and it returns that error.
func (filters Args) WalkValues(field string, op func(value string) error) error {