Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change 'docker run' exit codes to distinguish docker/contained errors #14012

Merged
merged 1 commit into from Nov 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 32 additions & 5 deletions api/client/run.go
Expand Up @@ -6,9 +6,11 @@ import (
"net/url"
"os"
"runtime"
"strings"

"github.com/Sirupsen/logrus"
Cli "github.com/docker/docker/cli"
derr "github.com/docker/docker/errors"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/signal"
Expand Down Expand Up @@ -36,6 +38,29 @@ func (cid *cidFile) Write(id string) error {
return nil
}

// if container start fails with 'command not found' error, return 127
// if container start fails with 'command cannot be invoked' error, return 126
// return 125 for generic docker daemon failures
func runStartContainerErr(err error) error {
trimmedErr := strings.Trim(err.Error(), "Error response from daemon: ")
statusError := Cli.StatusError{}
derrCmdNotFound := derr.ErrorCodeCmdNotFound.Message()
derrCouldNotInvoke := derr.ErrorCodeCmdCouldNotBeInvoked.Message()
derrNoSuchImage := derr.ErrorCodeNoSuchImageHash.Message()
derrNoSuchImageTag := derr.ErrorCodeNoSuchImageTag.Message()
switch trimmedErr {
case derrCmdNotFound:
statusError = Cli.StatusError{StatusCode: 127}
case derrCouldNotInvoke:
statusError = Cli.StatusError{StatusCode: 126}
case derrNoSuchImage, derrNoSuchImageTag:
statusError = Cli.StatusError{StatusCode: 125}
default:
statusError = Cli.StatusError{StatusCode: 125}
}
return statusError
}

// CmdRun runs a command in a new container.
//
// Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
Expand All @@ -60,7 +85,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
// just in case the Parse does not exit
if err != nil {
cmd.ReportError(err.Error(), true)
os.Exit(1)
os.Exit(125)
}

if len(hostConfig.DNS) > 0 {
Expand Down Expand Up @@ -115,7 +140,8 @@ func (cli *DockerCli) CmdRun(args ...string) error {

createResponse, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName)
if err != nil {
return err
cmd.ReportError(err.Error(), true)
return runStartContainerErr(err)
}
if sigProxy {
sigc := cli.forwardAllSignals(createResponse.ID)
Expand Down Expand Up @@ -199,8 +225,9 @@ func (cli *DockerCli) CmdRun(args ...string) error {
}()

//start the container
if _, _, err = readBody(cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)); err != nil {
return err
if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)); err != nil {
cmd.ReportError(err.Error(), false)
return runStartContainerErr(err)
}

if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut {
Expand Down Expand Up @@ -230,7 +257,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
// Autoremove: wait for the container to finish, retrieve
// the exit code and remove the container
if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/wait", nil, nil)); err != nil {
return err
return runStartContainerErr(err)
}
if _, status, err = getExitCode(cli, createResponse.ID); err != nil {
return err
Expand Down
25 changes: 24 additions & 1 deletion daemon/monitor.go
Expand Up @@ -3,13 +3,17 @@ package daemon
import (
"io"
"os/exec"
"strings"
"sync"
"syscall"
"time"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/execdriver"
derr "github.com/docker/docker/errors"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/runconfig"
"github.com/docker/docker/utils"
)

const (
Expand Down Expand Up @@ -163,11 +167,30 @@ func (m *containerMonitor) Start() error {
if exitStatus, err = m.supervisor.Run(m.container, pipes, m.callback); err != nil {
// 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 contained cmd not found/does not exist)
if strings.Contains(err.Error(), "executable file not found") ||
strings.Contains(err.Error(), "no such file or directory") ||
strings.Contains(err.Error(), "system cannot find the file specified") {
if m.container.RestartCount == 0 {
m.container.ExitCode = 127
m.resetContainer(false)
return derr.ErrorCodeCmdNotFound
}
}
// set to 126 for contained cmd can't be invoked errors
if strings.Contains(err.Error(), syscall.EACCES.Error()) {
if m.container.RestartCount == 0 {
m.container.ExitCode = 126
m.resetContainer(false)
return derr.ErrorCodeCmdCouldNotBeInvoked
}
}

if m.container.RestartCount == 0 {
m.container.ExitCode = -1
m.resetContainer(false)

return err
return derr.ErrorCodeCantStart.WithArgs(utils.GetErrorMessage(err))
}

logrus.Errorf("Error running container: %s", err)
Expand Down
3 changes: 1 addition & 2 deletions daemon/start.go
Expand Up @@ -7,7 +7,6 @@ import (
derr "github.com/docker/docker/errors"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/runconfig"
"github.com/docker/docker/utils"
)

// ContainerStart starts a container.
Expand Down Expand Up @@ -47,7 +46,7 @@ func (daemon *Daemon) ContainerStart(name string, hostConfig *runconfig.HostConf
}

if err := daemon.containerStart(container); err != nil {
return derr.ErrorCodeCantStart.WithArgs(name, utils.GetErrorMessage(err))
return err
}

return nil
Expand Down
32 changes: 32 additions & 0 deletions docs/reference/run.md
Expand Up @@ -518,6 +518,38 @@ non-zero exit status more than 10 times in a row Docker will abort trying to
restart the container. Providing a maximum restart limit is only valid for the
**on-failure** policy.

## Exit Status

The exit code from `docker run` gives information about why the container
failed to run or why it exited. When `docker run` exits with a non-zero code,
the exit codes follow the `chroot` standard, see below:

**_125_** if the error is with Docker daemon **_itself_**

$ docker run --foo busybox; echo $?
# flag provided but not defined: --foo
See 'docker run --help'.
125

**_126_** if the **_contained command_** cannot be invoked

$ docker run busybox /etc; echo $?
# exec: "/etc": permission denied
docker: Error response from daemon: Contained command could not be invoked
126

**_127_** if the **_contained command_** cannot be found

$ docker run busybox foo; echo $?
# exec: "foo": executable file not found in $PATH
docker: Error response from daemon: Contained command not found or does not exist
127

**_Exit code_** of **_contained command_** otherwise

$ docker run busybox /bin/sh -c 'exit 3'
# 3

## Clean up (--rm)

By default a container's file system persists even after the container
Expand Down
36 changes: 27 additions & 9 deletions errors/daemon.go
Expand Up @@ -599,15 +599,6 @@ var (
HTTPStatusCode: http.StatusInternalServerError,
})

// ErrorCodeCantStart is generated when an error occurred while
// trying to start a container.
ErrorCodeCantStart = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "CANTSTART",
Message: "Cannot start container %s: %s",
Description: "There was an error while trying to start a container",
HTTPStatusCode: http.StatusInternalServerError,
})

// ErrorCodeCantRestart is generated when an error occurred while
// trying to restart a container.
ErrorCodeCantRestart = errcode.Register(errGroup, errcode.ErrorDescriptor{
Expand Down Expand Up @@ -930,4 +921,31 @@ var (
Description: "An attempt to create a volume using a driver but the volume already exists with a different driver",
HTTPStatusCode: http.StatusInternalServerError,
})

// ErrorCodeCmdNotFound is generated when contained cmd can't start,
// contained command not found error, exit code 127
ErrorCodeCmdNotFound = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "CMDNOTFOUND",
Message: "Contained command not found or does not exist.",
Description: "Command could not be found, command does not exist",
HTTPStatusCode: http.StatusInternalServerError,
})

// ErrorCodeCmdCouldNotBeInvoked is generated when contained cmd can't start,
// contained command permission denied error, exit code 126
ErrorCodeCmdCouldNotBeInvoked = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "CMDCOULDNOTBEINVOKED",
Message: "Contained command could not be invoked.",
Description: "Permission denied, cannot invoke command",
HTTPStatusCode: http.StatusInternalServerError,
})

// ErrorCodeCantStart is generated when contained cmd can't start,
// for any reason other than above 2 errors
ErrorCodeCantStart = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "CANTSTART",
Message: "Cannot start container %s: %s",
Description: "There was an error while trying to start a container",
HTTPStatusCode: http.StatusInternalServerError,
})
)
69 changes: 62 additions & 7 deletions integration-cli/docker_cli_run_test.go
Expand Up @@ -1645,9 +1645,9 @@ func (s *DockerSuite) TestRunWorkdirExistsAndIsFile(c *check.C) {
expected = "The directory name is invalid"
}

out, exit, err := dockerCmdWithError("run", "-w", existingFile, "busybox")
if !(err != nil && exit == 1 && strings.Contains(out, expected)) {
c.Fatalf("Docker must complains about making dir, but we got out: %s, exit: %d, err: %s", out, exit, err)
out, exitCode, err := dockerCmdWithError("run", "-w", existingFile, "busybox")
if !(err != nil && exitCode == 125 && strings.Contains(out, expected)) {
c.Fatalf("Docker must complains about making dir with exitCode 125 but we got out: %s, exitCode: %d", out, exitCode)
}
}

Expand Down Expand Up @@ -3746,17 +3746,72 @@ func (s *DockerSuite) TestRunStdinBlockedAfterContainerExit(c *check.C) {
func (s *DockerSuite) TestRunWrongCpusetCpusFlagValue(c *check.C) {
// TODO Windows: This needs validation (error out) in the daemon.
testRequires(c, DaemonIsLinux)
out, _, err := dockerCmdWithError("run", "--cpuset-cpus", "1-10,11--", "busybox", "true")
out, exitCode, err := dockerCmdWithError("run", "--cpuset-cpus", "1-10,11--", "busybox", "true")
c.Assert(err, check.NotNil)
expected := "Error response from daemon: Invalid value 1-10,11-- for cpuset cpus.\n"
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
if !(strings.Contains(out, expected) || exitCode == 125) {
c.Fatalf("Expected output to contain %q with exitCode 125, got out: %q exitCode: %v", expected, out, exitCode)
}
}

func (s *DockerSuite) TestRunWrongCpusetMemsFlagValue(c *check.C) {
// TODO Windows: This needs validation (error out) in the daemon.
testRequires(c, DaemonIsLinux)
out, _, err := dockerCmdWithError("run", "--cpuset-mems", "1-42--", "busybox", "true")
out, exitCode, err := dockerCmdWithError("run", "--cpuset-mems", "1-42--", "busybox", "true")
c.Assert(err, check.NotNil)
expected := "Error response from daemon: Invalid value 1-42-- for cpuset mems.\n"
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
if !(strings.Contains(out, expected) || exitCode == 125) {
c.Fatalf("Expected output to contain %q with exitCode 125, got out: %q exitCode: %v", expected, out, exitCode)
}
}

// TestRunNonExecutableCmd checks that 'docker run busybox foo' exits with error code 127'
func (s *DockerSuite) TestRunNonExecutableCmd(c *check.C) {
name := "testNonExecutableCmd"
runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "foo")
_, exit, _ := runCommandWithOutput(runCmd)
stateExitCode := findContainerExitCode(c, name)
if !(exit == 127 && strings.Contains(stateExitCode, "127")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that you check both!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) thnx

c.Fatalf("Run non-executable command should have errored with exit code 127, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode)
}
}

// TestRunNonExistingCmd checks that 'docker run busybox /bin/foo' exits with code 127.
func (s *DockerSuite) TestRunNonExistingCmd(c *check.C) {
name := "testNonExistingCmd"
runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "/bin/foo")
_, exit, _ := runCommandWithOutput(runCmd)
stateExitCode := findContainerExitCode(c, name)
if !(exit == 127 && strings.Contains(stateExitCode, "127")) {
c.Fatalf("Run non-existing command should have errored with exit code 127, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode)
}
}

// TestCmdCannotBeInvoked checks that 'docker run busybox /etc' exits with 126.
func (s *DockerSuite) TestCmdCannotBeInvoked(c *check.C) {
name := "testCmdCannotBeInvoked"
runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "/etc")
_, exit, _ := runCommandWithOutput(runCmd)
stateExitCode := findContainerExitCode(c, name)
if !(exit == 126 && strings.Contains(stateExitCode, "126")) {
c.Fatalf("Run cmd that cannot be invoked should have errored with code 126, but we got exit: %d, State.ExitCode: %s", exit, stateExitCode)
}
}

// TestRunNonExistingImage checks that 'docker run foo' exits with error msg 125 and contains 'Unable to find image'
func (s *DockerSuite) TestRunNonExistingImage(c *check.C) {
runCmd := exec.Command(dockerBinary, "run", "foo")
out, exit, err := runCommandWithOutput(runCmd)
if !(err != nil && exit == 125 && strings.Contains(out, "Unable to find image")) {
c.Fatalf("Run non-existing image should have errored with 'Unable to find image' code 125, but we got out: %s, exit: %d, err: %s", out, exit, err)
}
}

// TestDockerFails checks that 'docker run -foo busybox' exits with 125 to signal docker run failed
func (s *DockerSuite) TestDockerFails(c *check.C) {
runCmd := exec.Command(dockerBinary, "run", "-foo", "busybox")
out, exit, err := runCommandWithOutput(runCmd)
if !(err != nil && exit == 125) {
c.Fatalf("Docker run with flag not defined should exit with 125, but we got out: %s, exit: %d, err: %s", out, exit, err)
}
}
12 changes: 8 additions & 4 deletions integration-cli/docker_cli_run_unix_test.go
Expand Up @@ -397,8 +397,10 @@ func (s *DockerSuite) TestRunInvalidCpusetCpusFlagValue(c *check.C) {
}
out, _, err := dockerCmdWithError("run", "--cpuset-cpus", strconv.Itoa(invalid), "busybox", "true")
c.Assert(err, check.NotNil)
expected := fmt.Sprintf("Error response from daemon: Requested CPUs are not available - requested %s, available: %s.\n", strconv.Itoa(invalid), sysInfo.Cpus)
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
expected := fmt.Sprintf("Error response from daemon: Requested CPUs are not available - requested %s, available: %s", strconv.Itoa(invalid), sysInfo.Cpus)
if !(strings.Contains(out, expected)) {
c.Fatalf("Expected output to contain %q, got %q", expected, out)
}
}

func (s *DockerSuite) TestRunInvalidCpusetMemsFlagValue(c *check.C) {
Expand All @@ -416,8 +418,10 @@ func (s *DockerSuite) TestRunInvalidCpusetMemsFlagValue(c *check.C) {
}
out, _, err := dockerCmdWithError("run", "--cpuset-mems", strconv.Itoa(invalid), "busybox", "true")
c.Assert(err, check.NotNil)
expected := fmt.Sprintf("Error response from daemon: Requested memory nodes are not available - requested %s, available: %s.\n", strconv.Itoa(invalid), sysInfo.Mems)
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
expected := fmt.Sprintf("Error response from daemon: Requested memory nodes are not available - requested %s, available: %s", strconv.Itoa(invalid), sysInfo.Mems)
if !(strings.Contains(out, expected)) {
c.Fatalf("Expected output to contain %q, got %q", expected, out)
}
}

func (s *DockerSuite) TestRunInvalidCPUShares(c *check.C) {
Expand Down
6 changes: 5 additions & 1 deletion integration-cli/docker_cli_start_test.go
Expand Up @@ -129,11 +129,15 @@ func (s *DockerSuite) TestStartMultipleContainers(c *check.C) {

// start all the three containers, container `child_first` start first which should be failed
// container 'parent' start second and then start container 'child_second'
expOut := "Cannot link to a non running container"
expErr := "failed to start containers: [child_first]"
out, _, err = dockerCmdWithError("start", "child_first", "parent", "child_second")
// err shouldn't be nil because start will fail
c.Assert(err, checker.NotNil, check.Commentf("out: %s", out))
// output does not correspond to what was expected
c.Assert(out, checker.Contains, "Cannot start container child_first")
if !(strings.Contains(out, expOut) || strings.Contains(err.Error(), expErr)) {
c.Fatalf("Expected out: %v with err: %v but got out: %v with err: %v", expOut, expErr, out, err)
}

for container, expected := range map[string]string{"parent": "true", "child_first": "false", "child_second": "true"} {
out, err := inspectField(container, "State.Running")
Expand Down
11 changes: 11 additions & 0 deletions integration-cli/docker_utils.go
Expand Up @@ -815,6 +815,17 @@ func dockerCmdInDirWithTimeout(timeout time.Duration, path string, args ...strin
return integration.DockerCmdInDirWithTimeout(dockerBinary, timeout, path, args...)
}

// find the State.ExitCode in container metadata
func findContainerExitCode(c *check.C, name string, vargs ...string) string {
args := append(vargs, "inspect", "--format='{{ .State.ExitCode }} {{ .State.Error }}'", name)
cmd := exec.Command(dockerBinary, args...)
out, _, err := runCommandWithOutput(cmd)
if err != nil {
c.Fatal(err, out)
}
return out
}

func findContainerIP(c *check.C, id string, network string) string {
out, _ := dockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.IPAddress }}'", network), id)
return strings.Trim(out, " \r\n'")
Expand Down