Implement docker exec with standalone client lib.

Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
David Calavera 2015-12-06 00:33:38 -05:00
parent bcb848c87f
commit 3f9f23114f
8 changed files with 87 additions and 67 deletions

View File

@ -15,10 +15,14 @@ import (
// apiClient is an interface that clients that talk with a docker server must implement.
type apiClient interface {
ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error)
ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error)
ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error)
ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error)
ContainerDiff(containerID string) ([]types.ContainerChange, error)
ContainerExecAttach(execID string, config runconfig.ExecConfig) (types.HijackedResponse, error)
ContainerExecCreate(config runconfig.ExecConfig) (types.ContainerExecCreateResponse, error)
ContainerExecInspect(execID string) (types.ContainerExecInspect, error)
ContainerExecStart(execID string, config types.ExecStartCheck) error
ContainerExport(containerID string) (io.ReadCloser, error)
ContainerInspect(containerID string) (types.ContainerJSON, error)
ContainerKill(containerID, signal string) error

View File

@ -1,7 +1,6 @@
package client
import (
"encoding/json"
"fmt"
"io"
@ -24,37 +23,29 @@ func (cli *DockerCli) CmdExec(args ...string) error {
return Cli.StatusError{StatusCode: 1}
}
serverResp, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, nil)
response, err := cli.client.ContainerExecCreate(*execConfig)
if err != nil {
return err
}
defer serverResp.body.Close()
var response types.ContainerExecCreateResponse
if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil {
return err
}
execID := response.ID
if execID == "" {
fmt.Fprintf(cli.out, "exec ID empty")
return nil
}
//Temp struct for execStart so that we don't need to transfer all the execConfig
execStartCheck := &types.ExecStartCheck{
Detach: execConfig.Detach,
Tty: execConfig.Tty,
}
if !execConfig.Detach {
if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
return err
}
} else {
if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execStartCheck, nil)); err != nil {
execStartCheck := types.ExecStartCheck{
Detach: execConfig.Detach,
Tty: execConfig.Tty,
}
if err := cli.client.ContainerExecStart(execID, execStartCheck); err != nil {
return err
}
// For now don't print this - wait for when we support exec wait()
@ -66,18 +57,9 @@ func (cli *DockerCli) CmdExec(args ...string) error {
var (
out, stderr io.Writer
in io.ReadCloser
hijacked = make(chan io.Closer)
errCh chan error
)
// Block the return until the chan gets closed
defer func() {
logrus.Debugf("End of CmdExec(), Waiting for hijack to finish.")
if _, ok := <-hijacked; ok {
fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)")
}
}()
if execConfig.AttachStdin {
in = cli.in
}
@ -91,24 +73,15 @@ func (cli *DockerCli) CmdExec(args ...string) error {
stderr = cli.err
}
}
errCh = promise.Go(func() error {
return cli.hijackWithContentType("POST", "/exec/"+execID+"/start", "application/json", execConfig.Tty, in, out, stderr, hijacked, execConfig)
})
// Acknowledge the hijack before starting
select {
case closer := <-hijacked:
// Make sure that hijack gets closed when returning. (result
// in closing hijack chan and freeing server's goroutines.
if closer != nil {
defer closer.Close()
}
case err := <-errCh:
if err != nil {
logrus.Debugf("Error hijack: %s", err)
return err
}
resp, err := cli.client.ContainerExecAttach(execID, *execConfig)
if err != nil {
return err
}
defer resp.Close()
errCh = promise.Go(func() error {
return cli.holdHijackedConnection(execConfig.Tty, in, out, stderr, resp)
})
if execConfig.Tty && cli.isTerminalIn {
if err := cli.monitorTtySize(execID, true); err != nil {

View File

@ -21,7 +21,7 @@ import (
"github.com/docker/docker/pkg/term"
)
func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp *types.HijackedResponse) error {
func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
var (
err error
oldState *term.State

View File

@ -10,7 +10,7 @@ import (
// It returns a types.HijackedConnection with the hijacked connection
// and the a reader to get output. It's up to the called to close
// the hijacked connection by calling types.HijackedResponse.Close.
func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error) {
func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error) {
query := url.Values{}
if options.Stream {
query.Set("stream", "1")

49
api/client/lib/exec.go Normal file
View File

@ -0,0 +1,49 @@
package lib
import (
"encoding/json"
"github.com/docker/docker/api/types"
"github.com/docker/docker/runconfig"
)
// ContainerExecCreate creates a new exec configuration to run an exec process.
func (cli *Client) ContainerExecCreate(config runconfig.ExecConfig) (types.ContainerExecCreateResponse, error) {
var response types.ContainerExecCreateResponse
resp, err := cli.post("/containers/"+config.Container+"/exec", nil, config, nil)
if err != nil {
return response, err
}
defer ensureReaderClosed(resp)
err = json.NewDecoder(resp.body).Decode(&response)
return response, err
}
// ContainerExecStart starts an exec process already create in the docker host.
func (cli *Client) ContainerExecStart(execID string, config types.ExecStartCheck) error {
resp, err := cli.post("/exec/"+execID+"/start", nil, config, nil)
ensureReaderClosed(resp)
return err
}
// ContainerExecAttach attaches a connection to an exec process in the server.
// It returns a types.HijackedConnection with the hijacked connection
// and the a reader to get output. It's up to the called to close
// the hijacked connection by calling types.HijackedResponse.Close.
func (cli *Client) ContainerExecAttach(execID string, config runconfig.ExecConfig) (types.HijackedResponse, error) {
headers := map[string][]string{"Content-Type": {"application/json"}}
return cli.postHijacked("/exec/"+execID+"/start", nil, config, headers)
}
// ContainerExecInspect returns information about a specific exec process on the docker host.
func (cli *Client) ContainerExecInspect(execID string) (types.ContainerExecInspect, error) {
var response types.ContainerExecInspect
resp, err := cli.get("/exec/"+execID+"/json", nil, nil)
if err != nil {
return response, err
}
defer ensureReaderClosed(resp)
err = json.NewDecoder(resp.body).Decode(&response)
return response, err
}

View File

@ -4,7 +4,6 @@ import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http/httputil"
"net/url"
@ -30,15 +29,15 @@ func (c *tlsClientCon) CloseWrite() error {
}
// postHijacked sends a POST request and hijacks the connection.
func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, headers map[string][]string) (*types.HijackedResponse, error) {
func (cli *Client) postHijacked(path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
bodyEncoded, err := encodeData(body)
if err != nil {
return nil, err
return types.HijackedResponse{}, err
}
req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
if err != nil {
return nil, err
return types.HijackedResponse{}, err
}
req.Host = cli.Addr
@ -48,9 +47,9 @@ func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, h
conn, err := dial(cli.Proto, cli.Addr, cli.tlsConfig)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
}
return nil, err
return types.HijackedResponse{}, err
}
// 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
rwc, br := clientconn.Hijack()
return &types.HijackedResponse{rwc, br}, nil
return types.HijackedResponse{rwc, br}, nil
}
func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {

View File

@ -278,29 +278,16 @@ func getExitCode(cli *DockerCli, containerID string) (bool, int, error) {
// 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) {
serverResp, err := cli.call("GET", "/exec/"+execID+"/json", nil, nil)
resp, err := cli.client.ContainerExecInspect(execID)
if err != nil {
// If we can't connect, then the daemon probably died.
if err != errConnectionFailed {
if err != lib.ErrConnectionFailed {
return false, -1, err
}
return false, -1, nil
}
defer serverResp.body.Close()
//TODO: Should we reconsider having a type in api/types?
//this is a response to exex/id/json not container
var c struct {
Running bool
ExitCode int
}
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
return false, -1, err
}
return c.Running, c.ExitCode, nil
return resp.Running, resp.ExitCode, nil
}
func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {

View File

@ -31,6 +31,14 @@ type ContainerCommitOptions struct {
JSONConfig string
}
// ContainerExecInspect holds information returned by exec inspect.
type ContainerExecInspect struct {
ExecID string
ContainerID string
Running bool
ExitCode int
}
// ContainerListOptions holds parameters to list containers with.
type ContainerListOptions struct {
Quiet bool