Explorar o código

Implement docker attach with standalone client lib.

Signed-off-by: David Calavera <david.calavera@gmail.com>
David Calavera %!s(int64=9) %!d(string=hai) anos
pai
achega
5e80ac9c84

+ 19 - 21
api/client/attach.go

@@ -1,10 +1,8 @@
 package client
 package client
 
 
 import (
 import (
-	"encoding/json"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
-	"net/url"
 
 
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
@@ -25,18 +23,11 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
 
 
 	cmd.ParseFlags(args, true)
 	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 {
 	if err != nil {
 		return err
 		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 {
 	if !c.State.Running {
 		return fmt.Errorf("You cannot attach to a stopped container, start it first")
 		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
 		in = cli.in
 	}
 	}
 
 
-	v.Set("stdout", "1")
-	v.Set("stderr", "1")
-
 	if *proxy && !c.Config.Tty {
 	if *proxy && !c.Config.Tty {
-		sigc := cli.forwardAllSignals(cmd.Arg(0))
+		sigc := cli.forwardAllSignals(options.ContainerID)
 		defer signal.StopCatch(sigc)
 		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
 		return err
 	}
 	}
 
 
-	_, status, err := getExitCode(cli, cmd.Arg(0))
+	_, status, err := getExitCode(cli, options.ContainerID)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 1 - 0
api/client/client.go

@@ -15,6 +15,7 @@ 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)
 	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)

+ 68 - 1
api/client/hijack.go

@@ -15,11 +15,79 @@ import (
 
 
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api"
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/dockerversion"
 	"github.com/docker/docker/dockerversion"
 	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/docker/docker/pkg/stdcopy"
 	"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 {
+	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 {
 type tlsClientCon struct {
 	*tls.Conn
 	*tls.Conn
 	rawConn net.Conn
 	rawConn net.Conn
@@ -190,7 +258,6 @@ func (cli *DockerCli) hijackWithContentType(method, path, contentType string, se
 	}
 	}
 
 
 	var oldState *term.State
 	var oldState *term.State
-
 	if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" {
 	if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" {
 		oldState, err = term.SetRawTerminal(cli.inFd)
 		oldState, err = term.SetRawTerminal(cli.inFd)
 		if err != nil {
 		if err != nil {

+ 4 - 0
api/client/lib/client.go

@@ -24,6 +24,8 @@ type Client struct {
 	BasePath string
 	BasePath string
 	// scheme holds the scheme of the client i.e. https.
 	// scheme holds the scheme of the client i.e. https.
 	Scheme string
 	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 holds the client transport instance. Exported to keep the old code running.
 	HTTPClient *http.Client
 	HTTPClient *http.Client
 	// version of the server to talk to.
 	// 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)
 	sockets.ConfigureTCPTransport(transport, proto, addr)
 
 
 	return &Client{
 	return &Client{
+		Proto:             proto,
 		Addr:              addr,
 		Addr:              addr,
 		BasePath:          basePath,
 		BasePath:          basePath,
 		Scheme:            scheme,
 		Scheme:            scheme,
+		tlsConfig:         tlsConfig,
 		HTTPClient:        &http.Client{Transport: transport},
 		HTTPClient:        &http.Client{Transport: transport},
 		version:           version,
 		version:           version,
 		customHTTPHeaders: httpHeaders,
 		customHTTPHeaders: httpHeaders,

+ 30 - 0
api/client/lib/container_attach.go

@@ -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 - 0
api/client/lib/hijack.go

@@ -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)
+}

+ 26 - 21
api/client/lib/request.go

@@ -71,38 +71,21 @@ func (cli *Client) sendRequest(method, path string, query url.Values, body inter
 	return cli.sendClientRequest(method, path, query, params, headers)
 	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{
 	serverResp := &serverResponse{
 		body:       nil,
 		body:       nil,
 		statusCode: -1,
 		statusCode: -1,
 	}
 	}
 
 
 	expectedPayload := (method == "POST" || method == "PUT")
 	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.Host = cli.Addr
 	req.URL.Scheme = cli.Scheme
 	req.URL.Scheme = cli.Scheme
 
 
-	if headers != nil {
-		for k, v := range headers {
-			req.Header[k] = v
-		}
-	}
-
 	if expectedPayload && req.Header.Get("Content-Type") == "" {
 	if expectedPayload && req.Header.Get("Content-Type") == "" {
 		req.Header.Set("Content-Type", "text/plain")
 		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
 	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) {
 func encodeData(data interface{}) (*bytes.Buffer, error) {
 	params := bytes.NewBuffer(nil)
 	params := bytes.NewBuffer(nil)
 	if data != nil {
 	if data != nil {

+ 37 - 1
api/types/client.go

@@ -1,14 +1,25 @@
 package types
 package types
 
 
 import (
 import (
+	"bufio"
 	"io"
 	"io"
+	"net"
 
 
 	"github.com/docker/docker/cliconfig"
 	"github.com/docker/docker/cliconfig"
 	"github.com/docker/docker/pkg/parsers/filters"
 	"github.com/docker/docker/pkg/parsers/filters"
 	"github.com/docker/docker/pkg/ulimit"
 	"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 {
 type ContainerCommitOptions struct {
 	ContainerID    string
 	ContainerID    string
 	RepositoryName string
 	RepositoryName string
@@ -67,6 +78,31 @@ type EventsOptions struct {
 	Filters filters.Args
 	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
 // ImageBuildOptions holds the information
 // necessary to build images.
 // necessary to build images.
 type ImageBuildOptions struct {
 type ImageBuildOptions struct {