Browse Source

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 <dug@us.ibm.com>
Doug Davis 10 years ago
parent
commit
90928eb114

+ 11 - 0
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 {
 		if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execConfig, false)); err != nil {
 			return err
 			return err
 		}
 		}
+		// For now don't print this - wait for when we support exec wait()
+		// fmt.Fprintf(cli.out, "%s\n", execID)
 		return nil
 		return nil
 	}
 	}
 
 
@@ -2636,5 +2638,14 @@ func (cli *DockerCli) CmdExec(args ...string) error {
 		return err
 		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
 	return nil
 }
 }

+ 20 - 0
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
 	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 {
 func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
 	cli.resizeTty(id, isExec)
 	cli.resizeTty(id, isExec)
 
 

+ 10 - 0
api/server/server.go

@@ -956,6 +956,15 @@ func getContainersByName(eng *engine.Engine, version version.Version, w http.Res
 	return job.Run()
 	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 {
 func getImagesByName(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if vars == nil {
 	if vars == nil {
 		return fmt.Errorf("Missing parameter")
 		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:.*}/top":       getContainersTop,
 			"/containers/{name:.*}/logs":      getContainersLogs,
 			"/containers/{name:.*}/logs":      getContainersLogs,
 			"/containers/{name:.*}/attach/ws": wsContainersAttach,
 			"/containers/{name:.*}/attach/ws": wsContainersAttach,
+			"/exec/{id:.*}/json":              getExecByID,
 		},
 		},
 		"POST": {
 		"POST": {
 			"/auth":                         postAuth,
 			"/auth":                         postAuth,

+ 4 - 0
daemon/container.go

@@ -602,6 +602,10 @@ func (container *Container) cleanup() {
 	if err := container.Unmount(); err != nil {
 	if err := container.Unmount(); err != nil {
 		log.Errorf("%v: Failed to umount filesystem: %v", container.ID, err)
 		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 {
 func (container *Container) KillSig(sig int) error {

+ 1 - 0
daemon/daemon.go

@@ -130,6 +130,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error {
 		"execCreate":        daemon.ContainerExecCreate,
 		"execCreate":        daemon.ContainerExecCreate,
 		"execStart":         daemon.ContainerExecStart,
 		"execStart":         daemon.ContainerExecStart,
 		"execResize":        daemon.ContainerExecResize,
 		"execResize":        daemon.ContainerExecResize,
+		"execInspect":       daemon.ContainerExecInspect,
 	} {
 	} {
 		if err := eng.Register(name, method); err != nil {
 		if err := eng.Register(name, method); err != nil {
 			return err
 			return err

+ 15 - 3
daemon/exec.go

@@ -24,6 +24,7 @@ type execConfig struct {
 	sync.Mutex
 	sync.Mutex
 	ID            string
 	ID            string
 	Running       bool
 	Running       bool
+	ExitCode      int
 	ProcessConfig execdriver.ProcessConfig
 	ProcessConfig execdriver.ProcessConfig
 	StreamConfig
 	StreamConfig
 	OpenStdin  bool
 	OpenStdin  bool
@@ -207,8 +208,9 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
 
 
 	execErr := make(chan error)
 	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() {
 	go func() {
 		err := container.Exec(execConfig)
 		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) {
 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 {
 func (container *Container) Exec(execConfig *execConfig) error {

+ 18 - 0
daemon/inspect.go

@@ -64,3 +64,21 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status {
 	}
 	}
 	return job.Errorf("No such container: %s", name)
 	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
+}

+ 108 - 0
docs/sources/reference/api/docker_remote_api_v1.16.md

@@ -1598,6 +1598,114 @@ Status Codes:
 -   **201** – no error
 -   **201** – no error
 -   **404** – no such exec instance
 -   **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. Going further
 
 
 ## 3.1 Inside `docker run`
 ## 3.1 Inside `docker run`

+ 17 - 0
integration-cli/docker_cli_exec_test.go

@@ -213,3 +213,20 @@ func TestExecEnv(t *testing.T) {
 
 
 	logDone("exec - exec inherits correct env")
 	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")
+}