Browse code

Implement docker exec with standalone client lib.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2015/12/06 14:33:38
Showing 8 changed files
... ...
@@ -15,10 +15,14 @@ import (
15 15
 
16 16
 // apiClient is an interface that clients that talk with a docker server must implement.
17 17
 type apiClient interface {
18
-	ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error)
18
+	ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error)
19 19
 	ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error)
20 20
 	ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error)
21 21
 	ContainerDiff(containerID string) ([]types.ContainerChange, error)
22
+	ContainerExecAttach(execID string, config runconfig.ExecConfig) (types.HijackedResponse, error)
23
+	ContainerExecCreate(config runconfig.ExecConfig) (types.ContainerExecCreateResponse, error)
24
+	ContainerExecInspect(execID string) (types.ContainerExecInspect, error)
25
+	ContainerExecStart(execID string, config types.ExecStartCheck) error
22 26
 	ContainerExport(containerID string) (io.ReadCloser, error)
23 27
 	ContainerInspect(containerID string) (types.ContainerJSON, error)
24 28
 	ContainerKill(containerID, signal string) error
... ...
@@ -1,7 +1,6 @@
1 1
 package client
2 2
 
3 3
 import (
4
-	"encoding/json"
5 4
 	"fmt"
6 5
 	"io"
7 6
 
... ...
@@ -24,37 +23,29 @@ func (cli *DockerCli) CmdExec(args ...string) error {
24 24
 		return Cli.StatusError{StatusCode: 1}
25 25
 	}
26 26
 
27
-	serverResp, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, nil)
27
+	response, err := cli.client.ContainerExecCreate(*execConfig)
28 28
 	if err != nil {
29 29
 		return err
30 30
 	}
31 31
 
32
-	defer serverResp.body.Close()
33
-
34
-	var response types.ContainerExecCreateResponse
35
-	if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil {
36
-		return err
37
-	}
38
-
39 32
 	execID := response.ID
40
-
41 33
 	if execID == "" {
42 34
 		fmt.Fprintf(cli.out, "exec ID empty")
43 35
 		return nil
44 36
 	}
45 37
 
46 38
 	//Temp struct for execStart so that we don't need to transfer all the execConfig
47
-	execStartCheck := &types.ExecStartCheck{
48
-		Detach: execConfig.Detach,
49
-		Tty:    execConfig.Tty,
50
-	}
51
-
52 39
 	if !execConfig.Detach {
53 40
 		if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
54 41
 			return err
55 42
 		}
56 43
 	} else {
57
-		if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execStartCheck, nil)); err != nil {
44
+		execStartCheck := types.ExecStartCheck{
45
+			Detach: execConfig.Detach,
46
+			Tty:    execConfig.Tty,
47
+		}
48
+
49
+		if err := cli.client.ContainerExecStart(execID, execStartCheck); err != nil {
58 50
 			return err
59 51
 		}
60 52
 		// For now don't print this - wait for when we support exec wait()
... ...
@@ -66,18 +57,9 @@ func (cli *DockerCli) CmdExec(args ...string) error {
66 66
 	var (
67 67
 		out, stderr io.Writer
68 68
 		in          io.ReadCloser
69
-		hijacked    = make(chan io.Closer)
70 69
 		errCh       chan error
71 70
 	)
72 71
 
73
-	// Block the return until the chan gets closed
74
-	defer func() {
75
-		logrus.Debugf("End of CmdExec(), Waiting for hijack to finish.")
76
-		if _, ok := <-hijacked; ok {
77
-			fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)")
78
-		}
79
-	}()
80
-
81 72
 	if execConfig.AttachStdin {
82 73
 		in = cli.in
83 74
 	}
... ...
@@ -91,24 +73,15 @@ func (cli *DockerCli) CmdExec(args ...string) error {
91 91
 			stderr = cli.err
92 92
 		}
93 93
 	}
94
-	errCh = promise.Go(func() error {
95
-		return cli.hijackWithContentType("POST", "/exec/"+execID+"/start", "application/json", execConfig.Tty, in, out, stderr, hijacked, execConfig)
96
-	})
97 94
 
98
-	// Acknowledge the hijack before starting
99
-	select {
100
-	case closer := <-hijacked:
101
-		// Make sure that hijack gets closed when returning. (result
102
-		// in closing hijack chan and freeing server's goroutines.
103
-		if closer != nil {
104
-			defer closer.Close()
105
-		}
106
-	case err := <-errCh:
107
-		if err != nil {
108
-			logrus.Debugf("Error hijack: %s", err)
109
-			return err
110
-		}
95
+	resp, err := cli.client.ContainerExecAttach(execID, *execConfig)
96
+	if err != nil {
97
+		return err
111 98
 	}
99
+	defer resp.Close()
100
+	errCh = promise.Go(func() error {
101
+		return cli.holdHijackedConnection(execConfig.Tty, in, out, stderr, resp)
102
+	})
112 103
 
113 104
 	if execConfig.Tty && cli.isTerminalIn {
114 105
 		if err := cli.monitorTtySize(execID, true); err != nil {
... ...
@@ -21,7 +21,7 @@ import (
21 21
 	"github.com/docker/docker/pkg/term"
22 22
 )
23 23
 
24
-func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp *types.HijackedResponse) error {
24
+func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
25 25
 	var (
26 26
 		err      error
27 27
 		oldState *term.State
... ...
@@ -10,7 +10,7 @@ import (
10 10
 // It returns a types.HijackedConnection with the hijacked connection
11 11
 // and the a reader to get output. It's up to the called to close
12 12
 // the hijacked connection by calling types.HijackedResponse.Close.
13
-func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error) {
13
+func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error) {
14 14
 	query := url.Values{}
15 15
 	if options.Stream {
16 16
 		query.Set("stream", "1")
17 17
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+package lib
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/runconfig"
7
+)
8
+
9
+// ContainerExecCreate creates a new exec configuration to run an exec process.
10
+func (cli *Client) ContainerExecCreate(config runconfig.ExecConfig) (types.ContainerExecCreateResponse, error) {
11
+	var response types.ContainerExecCreateResponse
12
+	resp, err := cli.post("/containers/"+config.Container+"/exec", nil, config, nil)
13
+	if err != nil {
14
+		return response, err
15
+	}
16
+	defer ensureReaderClosed(resp)
17
+	err = json.NewDecoder(resp.body).Decode(&response)
18
+	return response, err
19
+}
20
+
21
+// ContainerExecStart starts an exec process already create in the docker host.
22
+func (cli *Client) ContainerExecStart(execID string, config types.ExecStartCheck) error {
23
+	resp, err := cli.post("/exec/"+execID+"/start", nil, config, nil)
24
+	ensureReaderClosed(resp)
25
+	return err
26
+}
27
+
28
+// ContainerExecAttach attaches a connection to an exec process in the server.
29
+// It returns a types.HijackedConnection with the hijacked connection
30
+// and the a reader to get output. It's up to the called to close
31
+// the hijacked connection by calling types.HijackedResponse.Close.
32
+func (cli *Client) ContainerExecAttach(execID string, config runconfig.ExecConfig) (types.HijackedResponse, error) {
33
+	headers := map[string][]string{"Content-Type": {"application/json"}}
34
+	return cli.postHijacked("/exec/"+execID+"/start", nil, config, headers)
35
+}
36
+
37
+// ContainerExecInspect returns information about a specific exec process on the docker host.
38
+func (cli *Client) ContainerExecInspect(execID string) (types.ContainerExecInspect, error) {
39
+	var response types.ContainerExecInspect
40
+	resp, err := cli.get("/exec/"+execID+"/json", nil, nil)
41
+	if err != nil {
42
+		return response, err
43
+	}
44
+	defer ensureReaderClosed(resp)
45
+
46
+	err = json.NewDecoder(resp.body).Decode(&response)
47
+	return response, err
48
+}
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"crypto/tls"
5 5
 	"errors"
6 6
 	"fmt"
7
-	"io"
8 7
 	"net"
9 8
 	"net/http/httputil"
10 9
 	"net/url"
... ...
@@ -30,15 +29,15 @@ func (c *tlsClientCon) CloseWrite() error {
30 30
 }
31 31
 
32 32
 // postHijacked sends a POST request and hijacks the connection.
33
-func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, headers map[string][]string) (*types.HijackedResponse, error) {
33
+func (cli *Client) postHijacked(path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
34 34
 	bodyEncoded, err := encodeData(body)
35 35
 	if err != nil {
36
-		return nil, err
36
+		return types.HijackedResponse{}, err
37 37
 	}
38 38
 
39 39
 	req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
40 40
 	if err != nil {
41
-		return nil, err
41
+		return types.HijackedResponse{}, err
42 42
 	}
43 43
 	req.Host = cli.Addr
44 44
 
... ...
@@ -48,9 +47,9 @@ func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, h
48 48
 	conn, err := dial(cli.Proto, cli.Addr, cli.tlsConfig)
49 49
 	if err != nil {
50 50
 		if strings.Contains(err.Error(), "connection refused") {
51
-			return nil, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
51
+			return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
52 52
 		}
53
-		return nil, err
53
+		return types.HijackedResponse{}, err
54 54
 	}
55 55
 
56 56
 	// When we set up a TCP connection for hijack, there could be long periods
... ...
@@ -71,7 +70,7 @@ func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, h
71 71
 
72 72
 	rwc, br := clientconn.Hijack()
73 73
 
74
-	return &types.HijackedResponse{rwc, br}, nil
74
+	return types.HijackedResponse{rwc, br}, nil
75 75
 }
76 76
 
77 77
 func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
... ...
@@ -278,29 +278,16 @@ func getExitCode(cli *DockerCli, containerID string) (bool, int, error) {
278 278
 // getExecExitCode perform an inspect on the exec command. It returns
279 279
 // the running state and the exit code.
280 280
 func getExecExitCode(cli *DockerCli, execID string) (bool, int, error) {
281
-	serverResp, err := cli.call("GET", "/exec/"+execID+"/json", nil, nil)
281
+	resp, err := cli.client.ContainerExecInspect(execID)
282 282
 	if err != nil {
283 283
 		// If we can't connect, then the daemon probably died.
284
-		if err != errConnectionFailed {
284
+		if err != lib.ErrConnectionFailed {
285 285
 			return false, -1, err
286 286
 		}
287 287
 		return false, -1, nil
288 288
 	}
289 289
 
290
-	defer serverResp.body.Close()
291
-
292
-	//TODO: Should we reconsider having a type in api/types?
293
-	//this is a response to exex/id/json not container
294
-	var c struct {
295
-		Running  bool
296
-		ExitCode int
297
-	}
298
-
299
-	if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
300
-		return false, -1, err
301
-	}
302
-
303
-	return c.Running, c.ExitCode, nil
290
+	return resp.Running, resp.ExitCode, nil
304 291
 }
305 292
 
306 293
 func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
... ...
@@ -31,6 +31,14 @@ type ContainerCommitOptions struct {
31 31
 	JSONConfig     string
32 32
 }
33 33
 
34
+// ContainerExecInspect holds information returned by exec inspect.
35
+type ContainerExecInspect struct {
36
+	ExecID      string
37
+	ContainerID string
38
+	Running     bool
39
+	ExitCode    int
40
+}
41
+
34 42
 // ContainerListOptions holds parameters to list containers with.
35 43
 type ContainerListOptions struct {
36 44
 	Quiet  bool