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
Родитель abad24b6bc
Коммит 57d0053104
5 изменённых файлов: 262 добавлений и 22 удалений

Просмотреть файл

@ -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,

Просмотреть файл

@ -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
client/hijack.go Normal 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)
}

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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 {