Implement docker attach with standalone client lib.

Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
David Calavera 2015-12-05 22:22:00 -05:00
parent d9a62c5f2b
commit 5e80ac9c84
8 changed files with 350 additions and 44 deletions

View file

@ -1,10 +1,8 @@
package client
import (
"encoding/json"
"fmt"
"io"
"net/url"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
@ -25,18 +23,11 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
cmd.ParseFlags(args, true)
serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil)
c, err := cli.client.ContainerInspect(cmd.Arg(0))
if err != nil {
return err
}
defer serverResp.body.Close()
var c types.ContainerJSON
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
return err
}
if !c.State.Running {
return fmt.Errorf("You cannot attach to a stopped container, start it first")
}
@ -55,28 +46,35 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
}
}
var in io.ReadCloser
options := types.ContainerAttachOptions{
ContainerID: cmd.Arg(0),
Stream: true,
Stdin: !*noStdin && c.Config.OpenStdin,
Stdout: true,
Stderr: true,
}
v := url.Values{}
v.Set("stream", "1")
if !*noStdin && c.Config.OpenStdin {
v.Set("stdin", "1")
var in io.ReadCloser
if options.Stdin {
in = cli.in
}
v.Set("stdout", "1")
v.Set("stderr", "1")
if *proxy && !c.Config.Tty {
sigc := cli.forwardAllSignals(cmd.Arg(0))
sigc := cli.forwardAllSignals(options.ContainerID)
defer signal.StopCatch(sigc)
}
if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), c.Config.Tty, in, cli.out, cli.err, nil, nil); err != nil {
resp, err := cli.client.ContainerAttach(options)
if err != nil {
return err
}
defer resp.Close()
if err := cli.holdHijackedConnection(c.Config.Tty, in, cli.out, cli.err, resp); err != nil {
return err
}
_, status, err := getExitCode(cli, cmd.Arg(0))
_, status, err := getExitCode(cli, options.ContainerID)
if err != nil {
return err
}

View file

@ -15,6 +15,7 @@ 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)
ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error)
ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error)
ContainerDiff(containerID string) ([]types.ContainerChange, error)

View file

@ -15,11 +15,79 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/term"
)
func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp *types.HijackedResponse) error {
var (
err error
oldState *term.State
)
if inputStream != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" {
oldState, err = term.SetRawTerminal(cli.inFd)
if err != nil {
return err
}
defer term.RestoreTerminal(cli.inFd, oldState)
}
receiveStdout := make(chan error, 1)
if outputStream != nil || errorStream != nil {
go func() {
defer func() {
if inputStream != nil {
if setRawTerminal && cli.isTerminalIn {
term.RestoreTerminal(cli.inFd, oldState)
}
inputStream.Close()
}
}()
// When TTY is ON, use regular copy
if setRawTerminal && outputStream != nil {
_, err = io.Copy(outputStream, resp.Reader)
} else {
_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
}
logrus.Debugf("[hijack] End of stdout")
receiveStdout <- err
}()
}
stdinDone := make(chan struct{})
go func() {
if inputStream != nil {
io.Copy(resp.Conn, inputStream)
logrus.Debugf("[hijack] End of stdin")
}
if err := resp.CloseWrite(); err != nil {
logrus.Debugf("Couldn't send EOF: %s", err)
}
close(stdinDone)
}()
select {
case err := <-receiveStdout:
if err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
case <-stdinDone:
if outputStream != nil || errorStream != nil {
if err := <-receiveStdout; err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
}
}
return nil
}
type tlsClientCon struct {
*tls.Conn
rawConn net.Conn
@ -190,7 +258,6 @@ func (cli *DockerCli) hijackWithContentType(method, path, contentType string, se
}
var oldState *term.State
if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" {
oldState, err = term.SetRawTerminal(cli.inFd)
if err != nil {

View file

@ -24,6 +24,8 @@ type Client struct {
BasePath string
// scheme holds the scheme of the client i.e. https.
Scheme string
// tlsConfig holds the tls configuration to use in hijacked requests.
tlsConfig *tls.Config
// httpClient holds the client transport instance. Exported to keep the old code running.
HTTPClient *http.Client
// version of the server to talk to.
@ -78,9 +80,11 @@ func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsc
sockets.ConfigureTCPTransport(transport, proto, addr)
return &Client{
Proto: proto,
Addr: addr,
BasePath: basePath,
Scheme: scheme,
tlsConfig: tlsConfig,
HTTPClient: &http.Client{Transport: transport},
version: version,
customHTTPHeaders: httpHeaders,

View file

@ -0,0 +1,30 @@
package lib
import (
"net/url"
"github.com/docker/docker/api/types"
)
// ContainerAttach attaches a connection to a container 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) ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error) {
query := url.Values{}
if options.Stream {
query.Set("stream", "1")
}
if options.Stdin {
query.Set("stdin", "1")
}
if options.Stdout {
query.Set("stdout", "1")
}
if options.Stderr {
query.Set("stderr", "1")
}
headers := map[string][]string{"Content-Type": {"text/plain"}}
return cli.postHijacked("/containers/"+options.ContainerID+"/attach", query, nil, headers)
}

165
api/client/lib/hijack.go Normal file
View file

@ -0,0 +1,165 @@
package lib
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/docker/docker/api/types"
)
// tlsClientCon holds tls information and a dialed connection.
type tlsClientCon struct {
*tls.Conn
rawConn net.Conn
}
func (c *tlsClientCon) CloseWrite() error {
// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
// on its underlying connection.
if conn, ok := c.rawConn.(types.CloseWriter); ok {
return conn.CloseWrite()
}
return nil
}
// 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) {
bodyEncoded, err := encodeData(body)
if err != nil {
return nil, err
}
req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
if err != nil {
return nil, err
}
req.Host = cli.Addr
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "tcp")
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 nil, err
}
// When we set up a TCP connection for hijack, there could be long periods
// of inactivity (a long running command with no output) that in certain
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
// state. Setting TCP KeepAlive on the socket connection will prohibit
// ECONNTIMEOUT unless the socket connection truly is broken
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
}
clientconn := httputil.NewClientConn(conn, nil)
defer clientconn.Close()
// Server hijacks the connection, error 'connection closed' expected
clientconn.Do(req)
rwc, br := clientconn.Hijack()
return &types.HijackedResponse{rwc, br}, nil
}
func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
return tlsDialWithDialer(new(net.Dialer), network, addr, config)
}
// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
// order to return our custom tlsClientCon struct which holds both the tls.Conn
// object _and_ its underlying raw connection. The rationale for this is that
// we need to be able to close the write end of the connection when attaching,
// which tls.Conn does not provide.
func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
// We want the Timeout and Deadline values from dialer to cover the
// whole process: TCP connection and TLS handshake. This means that we
// also need to start our own timers now.
timeout := dialer.Timeout
if !dialer.Deadline.IsZero() {
deadlineTimeout := dialer.Deadline.Sub(time.Now())
if timeout == 0 || deadlineTimeout < timeout {
timeout = deadlineTimeout
}
}
var errChannel chan error
if timeout != 0 {
errChannel = make(chan error, 2)
time.AfterFunc(timeout, func() {
errChannel <- errors.New("")
})
}
rawConn, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
// When we set up a TCP connection for hijack, there could be long periods
// of inactivity (a long running command with no output) that in certain
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
// state. Setting TCP KeepAlive on the socket connection will prohibit
// ECONNTIMEOUT unless the socket connection truly is broken
if tcpConn, ok := rawConn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
}
colonPos := strings.LastIndex(addr, ":")
if colonPos == -1 {
colonPos = len(addr)
}
hostname := addr[:colonPos]
// If no ServerName is set, infer the ServerName
// from the hostname we're connecting to.
if config.ServerName == "" {
// Make a copy to avoid polluting argument or default.
c := *config
c.ServerName = hostname
config = &c
}
conn := tls.Client(rawConn, config)
if timeout == 0 {
err = conn.Handshake()
} else {
go func() {
errChannel <- conn.Handshake()
}()
err = <-errChannel
}
if err != nil {
rawConn.Close()
return nil, err
}
// This is Docker difference with standard's crypto/tls package: returned a
// wrapper which holds both the TLS and raw connections.
return &tlsClientCon{conn, rawConn}, nil
}
func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
if tlsConfig != nil && proto != "unix" {
// Notice this isn't Go standard's tls.Dial function
return tlsDial(proto, addr, tlsConfig)
}
return net.Dial(proto, addr)
}

View file

@ -71,38 +71,21 @@ func (cli *Client) sendRequest(method, path string, query url.Values, body inter
return cli.sendClientRequest(method, path, query, params, headers)
}
func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*serverResponse, error) {
func (cli *Client) sendClientRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) {
serverResp := &serverResponse{
body: nil,
statusCode: -1,
}
expectedPayload := (method == "POST" || method == "PUT")
if expectedPayload && in == nil {
in = bytes.NewReader([]byte{})
}
apiPath := cli.getAPIPath(path, query)
req, err := http.NewRequest(method, apiPath, in)
if err != nil {
return serverResp, err
}
// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
// then the user can't change OUR headers
for k, v := range cli.customHTTPHeaders {
req.Header.Set(k, v)
if expectedPayload && body == nil {
body = bytes.NewReader([]byte{})
}
req, err := cli.newRequest(method, path, query, body, headers)
req.URL.Host = cli.Addr
req.URL.Scheme = cli.Scheme
if headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
if expectedPayload && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "text/plain")
}
@ -143,6 +126,28 @@ func (cli *Client) sendClientRequest(method, path string, query url.Values, in i
return serverResp, nil
}
func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) {
apiPath := cli.getAPIPath(path, query)
req, err := http.NewRequest(method, apiPath, body)
if err != nil {
return nil, err
}
// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
// then the user can't change OUR headers
for k, v := range cli.customHTTPHeaders {
req.Header.Set(k, v)
}
if headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
return req, nil
}
func encodeData(data interface{}) (*bytes.Buffer, error) {
params := bytes.NewBuffer(nil)
if data != nil {

View file

@ -1,14 +1,25 @@
package types
import (
"bufio"
"io"
"net"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/pkg/parsers/filters"
"github.com/docker/docker/pkg/ulimit"
)
// ContainerCommitOptions hods parameters to commit changes into a container.
// ContainerAttachOptions holds parameters to attach to a container.
type ContainerAttachOptions struct {
ContainerID string
Stream bool
Stdin bool
Stdout bool
Stderr bool
}
// ContainerCommitOptions holds parameters to commit changes into a container.
type ContainerCommitOptions struct {
ContainerID string
RepositoryName string
@ -67,6 +78,31 @@ type EventsOptions struct {
Filters filters.Args
}
// HijackedResponse holds connection information for a hijacked request.
type HijackedResponse struct {
Conn net.Conn
Reader *bufio.Reader
}
// Close closes the hijacked connection and reader.
func (h *HijackedResponse) Close() {
h.Conn.Close()
}
// CloseWriter is an interface that implement structs
// that close input streams to prevent from writing.
type CloseWriter interface {
CloseWrite() error
}
// CloseWrite closes a readWriter for writing.
func (h *HijackedResponse) CloseWrite() error {
if conn, ok := h.Conn.(CloseWriter); ok {
return conn.CloseWrite()
}
return nil
}
// ImageBuildOptions holds the information
// necessary to build images.
type ImageBuildOptions struct {