Browse code

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 authored on 2014/11/18 08:50:09
Showing 9 changed files
... ...
@@ -2574,6 +2574,8 @@ func (cli *DockerCli) CmdExec(args ...string) error {
2574 2574
 		if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execConfig, false)); err != nil {
2575 2575
 			return err
2576 2576
 		}
2577
+		// For now don't print this - wait for when we support exec wait()
2578
+		// fmt.Fprintf(cli.out, "%s\n", execID)
2577 2579
 		return nil
2578 2580
 	}
2579 2581
 
... ...
@@ -2636,5 +2638,14 @@ func (cli *DockerCli) CmdExec(args ...string) error {
2636 2636
 		return err
2637 2637
 	}
2638 2638
 
2639
+	var status int
2640
+	if _, status, err = getExecExitCode(cli, execID); err != nil {
2641
+		return err
2642
+	}
2643
+
2644
+	if status != 0 {
2645
+		return &utils.StatusError{StatusCode: status}
2646
+	}
2647
+
2639 2648
 	return nil
2640 2649
 }
... ...
@@ -234,6 +234,26 @@ func getExitCode(cli *DockerCli, containerId string) (bool, int, error) {
234 234
 	return state.GetBool("Running"), state.GetInt("ExitCode"), nil
235 235
 }
236 236
 
237
+// getExecExitCode perform an inspect on the exec command. It returns
238
+// the running state and the exit code.
239
+func getExecExitCode(cli *DockerCli, execId string) (bool, int, error) {
240
+	stream, _, err := cli.call("GET", "/exec/"+execId+"/json", nil, false)
241
+	if err != nil {
242
+		// If we can't connect, then the daemon probably died.
243
+		if err != ErrConnectionRefused {
244
+			return false, -1, err
245
+		}
246
+		return false, -1, nil
247
+	}
248
+
249
+	var result engine.Env
250
+	if err := result.Decode(stream); err != nil {
251
+		return false, -1, err
252
+	}
253
+
254
+	return result.GetBool("Running"), result.GetInt("ExitCode"), nil
255
+}
256
+
237 257
 func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
238 258
 	cli.resizeTty(id, isExec)
239 259
 
... ...
@@ -956,6 +956,15 @@ func getContainersByName(eng *engine.Engine, version version.Version, w http.Res
956 956
 	return job.Run()
957 957
 }
958 958
 
959
+func getExecByID(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
960
+	if vars == nil {
961
+		return fmt.Errorf("Missing parameter 'id'")
962
+	}
963
+	var job = eng.Job("execInspect", vars["id"])
964
+	streamJSON(job, w, false)
965
+	return job.Run()
966
+}
967
+
959 968
 func getImagesByName(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
960 969
 	if vars == nil {
961 970
 		return fmt.Errorf("Missing parameter")
... ...
@@ -1277,6 +1286,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st
1277 1277
 			"/containers/{name:.*}/top":       getContainersTop,
1278 1278
 			"/containers/{name:.*}/logs":      getContainersLogs,
1279 1279
 			"/containers/{name:.*}/attach/ws": wsContainersAttach,
1280
+			"/exec/{id:.*}/json":              getExecByID,
1280 1281
 		},
1281 1282
 		"POST": {
1282 1283
 			"/auth":                         postAuth,
... ...
@@ -602,6 +602,10 @@ func (container *Container) cleanup() {
602 602
 	if err := container.Unmount(); err != nil {
603 603
 		log.Errorf("%v: Failed to umount filesystem: %v", container.ID, err)
604 604
 	}
605
+
606
+	for _, eConfig := range container.execCommands.s {
607
+		container.daemon.unregisterExecCommand(eConfig)
608
+	}
605 609
 }
606 610
 
607 611
 func (container *Container) KillSig(sig int) error {
... ...
@@ -130,6 +130,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error {
130 130
 		"execCreate":        daemon.ContainerExecCreate,
131 131
 		"execStart":         daemon.ContainerExecStart,
132 132
 		"execResize":        daemon.ContainerExecResize,
133
+		"execInspect":       daemon.ContainerExecInspect,
133 134
 	} {
134 135
 		if err := eng.Register(name, method); err != nil {
135 136
 			return err
... ...
@@ -24,6 +24,7 @@ type execConfig struct {
24 24
 	sync.Mutex
25 25
 	ID            string
26 26
 	Running       bool
27
+	ExitCode      int
27 28
 	ProcessConfig execdriver.ProcessConfig
28 29
 	StreamConfig
29 30
 	OpenStdin  bool
... ...
@@ -207,8 +208,9 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
207 207
 
208 208
 	execErr := make(chan error)
209 209
 
210
-	// Remove exec from daemon and container.
211
-	defer d.unregisterExecCommand(execConfig)
210
+	// Note, the execConfig data will be removed when the container
211
+	// itself is deleted.  This allows us to query it (for things like
212
+	// the exitStatus) even after the cmd is done running.
212 213
 
213 214
 	go func() {
214 215
 		err := container.Exec(execConfig)
... ...
@@ -231,7 +233,17 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
231 231
 }
232 232
 
233 233
 func (d *Daemon) Exec(c *Container, execConfig *execConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) {
234
-	return d.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback)
234
+	exitStatus, err := d.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback)
235
+
236
+	// On err, make sure we don't leave ExitCode at zero
237
+	if err != nil && exitStatus == 0 {
238
+		exitStatus = 128
239
+	}
240
+
241
+	execConfig.ExitCode = exitStatus
242
+	execConfig.Running = false
243
+
244
+	return exitStatus, err
235 245
 }
236 246
 
237 247
 func (container *Container) Exec(execConfig *execConfig) error {
... ...
@@ -64,3 +64,21 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status {
64 64
 	}
65 65
 	return job.Errorf("No such container: %s", name)
66 66
 }
67
+
68
+func (daemon *Daemon) ContainerExecInspect(job *engine.Job) engine.Status {
69
+	if len(job.Args) != 1 {
70
+		return job.Errorf("usage: %s ID", job.Name)
71
+	}
72
+	id := job.Args[0]
73
+	eConfig, err := daemon.getExecConfig(id)
74
+	if err != nil {
75
+		return job.Error(err)
76
+	}
77
+
78
+	b, err := json.Marshal(*eConfig)
79
+	if err != nil {
80
+		return job.Error(err)
81
+	}
82
+	job.Stdout.Write(b)
83
+	return engine.StatusOK
84
+}
... ...
@@ -1598,6 +1598,114 @@ Status Codes:
1598 1598
 -   **201** – no error
1599 1599
 -   **404** – no such exec instance
1600 1600
 
1601
+### Exec Inspect
1602
+
1603
+`GET /exec/(id)/json`
1604
+
1605
+Return low-level information about the exec command `id`.
1606
+
1607
+**Example request**:
1608
+
1609
+        GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1
1610
+
1611
+**Example response**:
1612
+
1613
+        HTTP/1.1 200 OK
1614
+        Content-Type: plain/text
1615
+
1616
+        {
1617
+          "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39",
1618
+          "Running" : false,
1619
+          "ExitCode" : 2,
1620
+          "ProcessConfig" : {
1621
+            "privileged" : false,
1622
+            "user" : "",
1623
+            "tty" : false,
1624
+            "entrypoint" : "sh",
1625
+            "arguments" : [
1626
+              "-c",
1627
+              "exit 2"
1628
+            ]
1629
+          },
1630
+          "OpenStdin" : false,
1631
+          "OpenStderr" : false,
1632
+          "OpenStdout" : false,
1633
+          "Container" : {
1634
+            "State" : {
1635
+              "Running" : true,
1636
+              "Paused" : false,
1637
+              "Restarting" : false,
1638
+              "OOMKilled" : false,
1639
+              "Pid" : 3650,
1640
+              "ExitCode" : 0,
1641
+              "Error" : "",
1642
+              "StartedAt" : "2014-11-17T22:26:03.717657531Z",
1643
+              "FinishedAt" : "0001-01-01T00:00:00Z"
1644
+            },
1645
+            "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c",
1646
+            "Created" : "2014-11-17T22:26:03.626304998Z",
1647
+            "Path" : "date",
1648
+            "Args" : [],
1649
+            "Config" : {
1650
+              "Hostname" : "8f177a186b97",
1651
+              "Domainname" : "",
1652
+              "User" : "",
1653
+              "Memory" : 0,
1654
+              "MemorySwap" : 0,
1655
+              "CpuShares" : 0,
1656
+              "Cpuset" : "",
1657
+              "AttachStdin" : false,
1658
+              "AttachStdout" : false,
1659
+              "AttachStderr" : false,
1660
+              "PortSpecs" : null,
1661
+              "ExposedPorts" : null,
1662
+              "Tty" : false,
1663
+              "OpenStdin" : false,
1664
+              "StdinOnce" : false,
1665
+              "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ],
1666
+              "Cmd" : [
1667
+                "date"
1668
+              ],
1669
+              "Image" : "ubuntu",
1670
+              "Volumes" : null,
1671
+              "WorkingDir" : "",
1672
+              "Entrypoint" : null,
1673
+              "NetworkDisabled" : false,
1674
+              "MacAddress" : "",
1675
+              "OnBuild" : null,
1676
+              "SecurityOpt" : null
1677
+            },
1678
+            "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5",
1679
+            "NetworkSettings" : {
1680
+              "IPAddress" : "172.17.0.2",
1681
+              "IPPrefixLen" : 16,
1682
+              "MacAddress" : "02:42:ac:11:00:02",
1683
+              "Gateway" : "172.17.42.1",
1684
+              "Bridge" : "docker0",
1685
+              "PortMapping" : null,
1686
+              "Ports" : {}
1687
+            },
1688
+            "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf",
1689
+            "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname",
1690
+            "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts",
1691
+            "Name" : "/test",
1692
+            "Driver" : "aufs",
1693
+            "ExecDriver" : "native-0.2",
1694
+            "MountLabel" : "",
1695
+            "ProcessLabel" : "",
1696
+            "AppArmorProfile" : "",
1697
+            "RestartCount" : 0,
1698
+            "Volumes" : {},
1699
+            "VolumesRW" : {}
1700
+          }
1701
+        }
1702
+
1703
+Status Codes:
1704
+
1705
+-   **200** – no error
1706
+-   **404** – no such exec instance
1707
+-   **500** - server error
1708
+
1601 1709
 # 3. Going further
1602 1710
 
1603 1711
 ## 3.1 Inside `docker run`
... ...
@@ -213,3 +213,20 @@ func TestExecEnv(t *testing.T) {
213 213
 
214 214
 	logDone("exec - exec inherits correct env")
215 215
 }
216
+
217
+func TestExecExitStatus(t *testing.T) {
218
+	runCmd := exec.Command(dockerBinary, "run", "-d", "--name", "top", "busybox", "top")
219
+	if out, _, _, err := runCommandWithStdoutStderr(runCmd); err != nil {
220
+		t.Fatal(out, err)
221
+	}
222
+
223
+	// Test normal (non-detached) case first
224
+	cmd := exec.Command(dockerBinary, "exec", "top", "sh", "-c", "exit 23")
225
+	ec, _ := runCommand(cmd)
226
+
227
+	if ec != 23 {
228
+		t.Fatalf("Should have had an ExitCode of 23, not: %d", ec)
229
+	}
230
+
231
+	logDone("exec - exec non-zero ExitStatus")
232
+}