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. // apiClient is an interface that clients that talk with a docker server must implement.
type apiClient interface { type apiClient interface {
ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error) ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error)
ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error)
ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error) ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error)
ContainerDiff(containerID string) ([]types.ContainerChange, 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) ContainerExport(containerID string) (io.ReadCloser, error)
ContainerInspect(containerID string) (types.ContainerJSON, error) ContainerInspect(containerID string) (types.ContainerJSON, error)
ContainerKill(containerID, signal string) error ContainerKill(containerID, signal string) error

View File

@ -1,7 +1,6 @@
package client package client
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
@ -24,37 +23,29 @@ func (cli *DockerCli) CmdExec(args ...string) error {
return Cli.StatusError{StatusCode: 1} 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 { if err != nil {
return err 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 execID := response.ID
if execID == "" { if execID == "" {
fmt.Fprintf(cli.out, "exec ID empty") fmt.Fprintf(cli.out, "exec ID empty")
return nil return nil
} }
//Temp struct for execStart so that we don't need to transfer all the execConfig //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 !execConfig.Detach {
if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil { if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
return err return err
} }
} else { } 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 return err
} }
// For now don't print this - wait for when we support exec wait() // 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 ( var (
out, stderr io.Writer out, stderr io.Writer
in io.ReadCloser in io.ReadCloser
hijacked = make(chan io.Closer)
errCh chan error 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 { if execConfig.AttachStdin {
in = cli.in in = cli.in
} }
@ -91,24 +73,15 @@ func (cli *DockerCli) CmdExec(args ...string) error {
stderr = cli.err 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 resp, err := cli.client.ContainerExecAttach(execID, *execConfig)
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 { if err != nil {
logrus.Debugf("Error hijack: %s", err)
return err 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 execConfig.Tty && cli.isTerminalIn {
if err := cli.monitorTtySize(execID, true); err != nil { if err := cli.monitorTtySize(execID, true); err != nil {

View File

@ -21,7 +21,7 @@ import (
"github.com/docker/docker/pkg/term" "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 ( var (
err error err error
oldState *term.State oldState *term.State

View File

@ -10,7 +10,7 @@ import (
// It returns a types.HijackedConnection with the hijacked connection // It returns a types.HijackedConnection with the hijacked connection
// and the a reader to get output. It's up to the called to close // and the a reader to get output. It's up to the called to close
// the hijacked connection by calling types.HijackedResponse.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{} query := url.Values{}
if options.Stream { if options.Stream {
query.Set("stream", "1") 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" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@ -30,15 +29,15 @@ func (c *tlsClientCon) CloseWrite() error {
} }
// postHijacked sends a POST request and hijacks the connection. // 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) bodyEncoded, err := encodeData(body)
if err != nil { if err != nil {
return nil, err return types.HijackedResponse{}, err
} }
req, err := cli.newRequest("POST", path, query, bodyEncoded, headers) req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
if err != nil { if err != nil {
return nil, err return types.HijackedResponse{}, err
} }
req.Host = cli.Addr 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) conn, err := dial(cli.Proto, cli.Addr, cli.tlsConfig)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "connection refused") { 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 // 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() 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) { 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 // getExecExitCode perform an inspect on the exec command. It returns
// the running state and the exit code. // the running state and the exit code.
func getExecExitCode(cli *DockerCli, execID string) (bool, int, error) { 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 err != nil {
// If we can't connect, then the daemon probably died. // If we can't connect, then the daemon probably died.
if err != errConnectionFailed { if err != lib.ErrConnectionFailed {
return false, -1, err return false, -1, err
} }
return false, -1, nil return false, -1, nil
} }
defer serverResp.body.Close() return resp.Running, resp.ExitCode, nil
//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
} }
func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {

View File

@ -31,6 +31,14 @@ type ContainerCommitOptions struct {
JSONConfig string 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. // ContainerListOptions holds parameters to list containers with.
type ContainerListOptions struct { type ContainerListOptions struct {
Quiet bool Quiet bool