From 90928eb1140fc0394e2a79d5e9a91dbc0f02484c Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Mon, 17 Nov 2014 15:50:09 -0800 Subject: [PATCH] Add support for docker exec to return cmd exitStatus Note - only support the non-detached mode of exec right now. Another PR will add -d support. Closes #8703 Signed-off-by: Doug Davis --- api/client/commands.go | 11 ++ api/client/utils.go | 20 ++++ api/server/server.go | 10 ++ daemon/container.go | 4 + daemon/daemon.go | 1 + daemon/exec.go | 18 ++- daemon/inspect.go | 18 +++ .../reference/api/docker_remote_api_v1.16.md | 108 ++++++++++++++++++ integration-cli/docker_cli_exec_test.go | 17 +++ 9 files changed, 204 insertions(+), 3 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 6ddae4a3a7..ddff3d88a8 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2574,6 +2574,8 @@ func (cli *DockerCli) CmdExec(args ...string) error { if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execConfig, false)); err != nil { return err } + // For now don't print this - wait for when we support exec wait() + // fmt.Fprintf(cli.out, "%s\n", execID) return nil } @@ -2636,5 +2638,14 @@ func (cli *DockerCli) CmdExec(args ...string) error { return err } + var status int + if _, status, err = getExecExitCode(cli, execID); err != nil { + return err + } + + if status != 0 { + return &utils.StatusError{StatusCode: status} + } + return nil } diff --git a/api/client/utils.go b/api/client/utils.go index 3799ce6735..8de571bf4d 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -234,6 +234,26 @@ func getExitCode(cli *DockerCli, containerId string) (bool, int, error) { return state.GetBool("Running"), state.GetInt("ExitCode"), nil } +// getExecExitCode perform an inspect on the exec command. It returns +// the running state and the exit code. +func getExecExitCode(cli *DockerCli, execId string) (bool, int, error) { + stream, _, err := cli.call("GET", "/exec/"+execId+"/json", nil, false) + if err != nil { + // If we can't connect, then the daemon probably died. + if err != ErrConnectionRefused { + return false, -1, err + } + return false, -1, nil + } + + var result engine.Env + if err := result.Decode(stream); err != nil { + return false, -1, err + } + + return result.GetBool("Running"), result.GetInt("ExitCode"), nil +} + func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { cli.resizeTty(id, isExec) diff --git a/api/server/server.go b/api/server/server.go index d9b73e6798..1d591a3d84 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -956,6 +956,15 @@ func getContainersByName(eng *engine.Engine, version version.Version, w http.Res return job.Run() } +func getExecByID(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if vars == nil { + return fmt.Errorf("Missing parameter 'id'") + } + var job = eng.Job("execInspect", vars["id"]) + streamJSON(job, w, false) + return job.Run() +} + func getImagesByName(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") @@ -1277,6 +1286,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st "/containers/{name:.*}/top": getContainersTop, "/containers/{name:.*}/logs": getContainersLogs, "/containers/{name:.*}/attach/ws": wsContainersAttach, + "/exec/{id:.*}/json": getExecByID, }, "POST": { "/auth": postAuth, diff --git a/daemon/container.go b/daemon/container.go index bf93787ebf..b35969900c 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -602,6 +602,10 @@ func (container *Container) cleanup() { if err := container.Unmount(); err != nil { log.Errorf("%v: Failed to umount filesystem: %v", container.ID, err) } + + for _, eConfig := range container.execCommands.s { + container.daemon.unregisterExecCommand(eConfig) + } } func (container *Container) KillSig(sig int) error { diff --git a/daemon/daemon.go b/daemon/daemon.go index 84628be729..06dc557799 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -130,6 +130,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { "execCreate": daemon.ContainerExecCreate, "execStart": daemon.ContainerExecStart, "execResize": daemon.ContainerExecResize, + "execInspect": daemon.ContainerExecInspect, } { if err := eng.Register(name, method); err != nil { return err diff --git a/daemon/exec.go b/daemon/exec.go index d813dbba1d..71529b165c 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -24,6 +24,7 @@ type execConfig struct { sync.Mutex ID string Running bool + ExitCode int ProcessConfig execdriver.ProcessConfig StreamConfig OpenStdin bool @@ -207,8 +208,9 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status { execErr := make(chan error) - // Remove exec from daemon and container. - defer d.unregisterExecCommand(execConfig) + // Note, the execConfig data will be removed when the container + // itself is deleted. This allows us to query it (for things like + // the exitStatus) even after the cmd is done running. go func() { err := container.Exec(execConfig) @@ -231,7 +233,17 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status { } func (d *Daemon) Exec(c *Container, execConfig *execConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { - return d.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback) + exitStatus, err := d.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback) + + // On err, make sure we don't leave ExitCode at zero + if err != nil && exitStatus == 0 { + exitStatus = 128 + } + + execConfig.ExitCode = exitStatus + execConfig.Running = false + + return exitStatus, err } func (container *Container) Exec(execConfig *execConfig) error { diff --git a/daemon/inspect.go b/daemon/inspect.go index 396ca0227f..5dec257f3a 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -64,3 +64,21 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status { } return job.Errorf("No such container: %s", name) } + +func (daemon *Daemon) ContainerExecInspect(job *engine.Job) engine.Status { + if len(job.Args) != 1 { + return job.Errorf("usage: %s ID", job.Name) + } + id := job.Args[0] + eConfig, err := daemon.getExecConfig(id) + if err != nil { + return job.Error(err) + } + + b, err := json.Marshal(*eConfig) + if err != nil { + return job.Error(err) + } + job.Stdout.Write(b) + return engine.StatusOK +} diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index dc2cc56267..15a8f1c4b5 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -1598,6 +1598,114 @@ Status Codes: - **201** – no error - **404** – no such exec instance +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the exec command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "Memory" : 0, + "MemorySwap" : 0, + "CpuShares" : 0, + "Cpuset" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "PortSpecs" : null, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings" : { + "IPAddress" : "172.17.0.2", + "IPPrefixLen" : 16, + "MacAddress" : "02:42:ac:11:00:02", + "Gateway" : "172.17.42.1", + "Bridge" : "docker0", + "PortMapping" : null, + "Ports" : {} + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Volumes" : {}, + "VolumesRW" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + # 3. Going further ## 3.1 Inside `docker run` diff --git a/integration-cli/docker_cli_exec_test.go b/integration-cli/docker_cli_exec_test.go index 438271744a..ebb5484f2e 100644 --- a/integration-cli/docker_cli_exec_test.go +++ b/integration-cli/docker_cli_exec_test.go @@ -213,3 +213,20 @@ func TestExecEnv(t *testing.T) { logDone("exec - exec inherits correct env") } + +func TestExecExitStatus(t *testing.T) { + runCmd := exec.Command(dockerBinary, "run", "-d", "--name", "top", "busybox", "top") + if out, _, _, err := runCommandWithStdoutStderr(runCmd); err != nil { + t.Fatal(out, err) + } + + // Test normal (non-detached) case first + cmd := exec.Command(dockerBinary, "exec", "top", "sh", "-c", "exit 23") + ec, _ := runCommand(cmd) + + if ec != 23 { + t.Fatalf("Should have had an ExitCode of 23, not: %d", ec) + } + + logDone("exec - exec non-zero ExitStatus") +}