dotcloud/docker

View on GitHub
daemon/errors.go

Summary

Maintainability
A
45 mins
Test Coverage
package daemon // import "github.com/docker/docker/daemon"

import (
    "fmt"
    "strings"
    "syscall"

    "github.com/docker/docker/errdefs"
    "github.com/pkg/errors"
    "google.golang.org/grpc/status"
)

func isNotRunning(err error) bool {
    var nre *containerNotRunningError
    return errors.As(err, &nre)
}

func errNotRunning(id string) error {
    return &containerNotRunningError{errors.Errorf("container %s is not running", id)}
}

type containerNotRunningError struct {
    error
}

func (e containerNotRunningError) Conflict() {}

func containerNotFound(id string) error {
    return objNotFoundError{"container", id}
}

type objNotFoundError struct {
    object string
    id     string
}

func (e objNotFoundError) Error() string {
    return "No such " + e.object + ": " + e.id
}

func (e objNotFoundError) NotFound() {}

func errContainerIsRestarting(containerID string) error {
    cause := errors.Errorf("Container %s is restarting, wait until the container is running", containerID)
    return errdefs.Conflict(cause)
}

func errExecNotFound(id string) error {
    return objNotFoundError{"exec instance", id}
}

func errExecPaused(id string) error {
    cause := errors.Errorf("Container %s is paused, unpause the container before exec", id)
    return errdefs.Conflict(cause)
}

func errNotPaused(id string) error {
    cause := errors.Errorf("Container %s is already paused", id)
    return errdefs.Conflict(cause)
}

type nameConflictError struct {
    id   string
    name string
}

func (e nameConflictError) Error() string {
    return fmt.Sprintf("Conflict. The container name %q is already in use by container %q. You have to remove (or rename) that container to be able to reuse that name.", e.name, e.id)
}

func (nameConflictError) Conflict() {}

type invalidIdentifier string

func (e invalidIdentifier) Error() string {
    return fmt.Sprintf("invalid name or ID supplied: %q", string(e))
}

func (invalidIdentifier) InvalidParameter() {}

type incompatibleDeviceRequest struct {
    driver string
    caps   [][]string
}

func (i incompatibleDeviceRequest) Error() string {
    return fmt.Sprintf("could not select device driver %q with capabilities: %v", i.driver, i.caps)
}

func (incompatibleDeviceRequest) InvalidParameter() {}

type duplicateMountPointError string

func (e duplicateMountPointError) Error() string {
    return "Duplicate mount point: " + string(e)
}
func (duplicateMountPointError) InvalidParameter() {}

type containerFileNotFound struct {
    file      string
    container string
}

func (e containerFileNotFound) Error() string {
    return "Could not find the file " + e.file + " in container " + e.container
}

func (containerFileNotFound) NotFound() {}

type startInvalidConfigError string

func (e startInvalidConfigError) Error() string {
    return string(e)
}

func (e startInvalidConfigError) InvalidParameter() {} // Is this right???

// exitStatus is the exit-code as set by setExitCodeFromError
type exitStatus = int

const (
    exitEaccess     exitStatus = 126 // container cmd can't be invoked (permission denied)
    exitCmdNotFound exitStatus = 127 // container cmd not found/does not exist or invalid bind-mount
    exitUnknown     exitStatus = 128 // unknown error
)

// setExitCodeFromError converts the error returned by containerd
// when starting a container, and applies the corresponding exitStatus to the
// container. It returns an errdefs error (either errdefs.ErrInvalidParameter
// or errdefs.ErrUnknown).
func setExitCodeFromError(setExitCode func(exitStatus), err error) error {
    if err == nil {
        return nil
    }
    errDesc := status.Convert(err).Message()
    contains := func(s1, s2 string) bool {
        return strings.Contains(strings.ToLower(s1), s2)
    }

    // set to 126 for container cmd can't be invoked errors
    if contains(errDesc, syscall.EACCES.Error()) {
        setExitCode(exitEaccess)
        return startInvalidConfigError(errDesc)
    }

    // Go 1.20 changed the error for attempting to execute a directory from
    // syscall.EACCESS to syscall.EISDIR. Unfortunately docker/cli checks
    // whether the error message contains syscall.EACCESS.Error() to
    // determine whether to exit with code 126 or 125, so we have little
    // choice but to fudge the error string.
    if contains(errDesc, syscall.EISDIR.Error()) {
        errDesc += ": " + syscall.EACCES.Error()
        setExitCode(exitEaccess)
        return startInvalidConfigError(errDesc)
    }

    // attempted to mount a file onto a directory, or a directory onto a file, maybe from user specified bind mounts
    if contains(errDesc, syscall.ENOTDIR.Error()) {
        errDesc += ": Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type"
        setExitCode(exitCmdNotFound)
        return startInvalidConfigError(errDesc)
    }

    // if we receive an internal error from the initial start of a container then lets
    // return it instead of entering the restart loop
    // set to 127 for container cmd not found/does not exist.
    if isInvalidCommand(errDesc) {
        setExitCode(exitCmdNotFound)
        return startInvalidConfigError(errDesc)
    }

    // TODO: it would be nice to get some better errors from containerd so we can return better errors here
    setExitCode(exitUnknown)
    return errdefs.Unknown(errors.New(errDesc))
}

// isInvalidCommand tries to detect if the reason the container failed to start
// was due to an invalid command for the container (command not found, or not
// a valid executable).
func isInvalidCommand(errMessage string) bool {
    errMessage = strings.ToLower(errMessage)
    errMessages := []string{
        "executable file not found",
        "no such file or directory",
        "system cannot find the file specified",
        "failed to run runc create/exec call",
    }

    for _, msg := range errMessages {
        if strings.Contains(errMessage, msg) {
            return true
        }
    }
    return false
}