Bladeren bron

Extract API client struct as standalone client.

Signed-off-by: David Calavera <david.calavera@gmail.com>
David Calavera 9 jaren geleden
bovenliggende
commit
589df17a1a
4 gewijzigde bestanden met toevoegingen van 319 en 63 verwijderingen
  1. 58 62
      api/client/cli.go
  2. 98 0
      api/client/lib/client.go
  3. 155 0
      api/client/lib/request.go
  4. 8 1
      cliconfig/config.go

+ 58 - 62
api/client/cli.go

@@ -6,14 +6,14 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"net/url"
 	"os"
-	"strings"
+	"runtime"
 
+	"github.com/docker/docker/api/client/lib"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cliconfig"
+	"github.com/docker/docker/dockerversion"
 	"github.com/docker/docker/opts"
-	"github.com/docker/docker/pkg/sockets"
 	"github.com/docker/docker/pkg/term"
 	"github.com/docker/docker/pkg/tlsconfig"
 )
@@ -24,13 +24,6 @@ type DockerCli struct {
 	// initializing closure
 	init func() error
 
-	// proto holds the client protocol i.e. unix.
-	proto string
-	// addr holds the client address.
-	addr string
-	// basePath holds the path to prepend to the requests
-	basePath string
-
 	// configFile has the client configuration file
 	configFile *cliconfig.ConfigFile
 	// in holds the input stream and closer (io.ReadCloser) for the client.
@@ -41,11 +34,6 @@ type DockerCli struct {
 	err io.Writer
 	// keyFile holds the key file as a string.
 	keyFile string
-	// tlsConfig holds the TLS configuration for the client, and will
-	// set the scheme to https in NewDockerCli if present.
-	tlsConfig *tls.Config
-	// scheme holds the scheme of the client i.e. https.
-	scheme string
 	// inFd holds the file descriptor of the client's STDIN (if valid).
 	inFd uintptr
 	// outFd holds file descriptor of the client's STDOUT (if valid).
@@ -54,6 +42,22 @@ type DockerCli struct {
 	isTerminalIn bool
 	// isTerminalOut indicates whether the client's STDOUT is a TTY
 	isTerminalOut bool
+	// client is the http client that performs all API operations
+	client *lib.Client
+
+	// DEPRECATED OPTIONS TO MAKE THE CLIENT COMPILE
+	// TODO: Remove
+	// proto holds the client protocol i.e. unix.
+	proto string
+	// addr holds the client address.
+	addr string
+	// basePath holds the path to prepend to the requests
+	basePath string
+	// tlsConfig holds the TLS configuration for the client, and will
+	// set the scheme to https in NewDockerCli if present.
+	tlsConfig *tls.Config
+	// scheme holds the scheme of the client i.e. https.
+	scheme string
 	// transport holds the client transport instance.
 	transport *http.Transport
 }
@@ -98,50 +102,35 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
 	}
 
 	cli.init = func() error {
-
 		clientFlags.PostParse()
-
-		hosts := clientFlags.Common.Hosts
-
-		switch len(hosts) {
-		case 0:
-			hosts = []string{os.Getenv("DOCKER_HOST")}
-		case 1:
-			// only accept one host to talk to
-		default:
-			return errors.New("Please specify only one -H")
+		configFile, e := cliconfig.Load(cliconfig.ConfigDir())
+		if e != nil {
+			fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
 		}
+		cli.configFile = configFile
 
-		defaultHost := opts.DefaultTCPHost
-		if clientFlags.Common.TLSOptions != nil {
-			defaultHost = opts.DefaultTLSHost
+		host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions)
+		if err != nil {
+			return err
 		}
 
-		var e error
-		if hosts[0], e = opts.ParseHost(defaultHost, hosts[0]); e != nil {
-			return e
+		customHeaders := cli.configFile.HTTPHeaders
+		if customHeaders == nil {
+			customHeaders = map[string]string{}
 		}
+		customHeaders["User-Agent"] = "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
 
-		protoAddrParts := strings.SplitN(hosts[0], "://", 2)
-		cli.proto, cli.addr = protoAddrParts[0], protoAddrParts[1]
-
-		if cli.proto == "tcp" {
-			// error is checked in pkg/parsers already
-			parsed, _ := url.Parse("tcp://" + cli.addr)
-			cli.addr = parsed.Host
-			cli.basePath = parsed.Path
+		client, err := lib.NewClient(host, clientFlags.Common.TLSOptions, customHeaders)
+		if err != nil {
+			return err
 		}
+		cli.client = client
 
-		if clientFlags.Common.TLSOptions != nil {
-			cli.scheme = "https"
-			var e error
-			cli.tlsConfig, e = tlsconfig.Client(*clientFlags.Common.TLSOptions)
-			if e != nil {
-				return e
-			}
-		} else {
-			cli.scheme = "http"
-		}
+		// FIXME: Deprecated, only to keep the old code running.
+		cli.transport = client.HTTPClient.Transport.(*http.Transport)
+		cli.basePath = client.BasePath
+		cli.addr = client.Addr
+		cli.scheme = client.Scheme
 
 		if cli.in != nil {
 			cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in)
@@ -150,20 +139,27 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
 			cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out)
 		}
 
-		// The transport is created here for reuse during the client session.
-		cli.transport = &http.Transport{
-			TLSClientConfig: cli.tlsConfig,
-		}
-		sockets.ConfigureTCPTransport(cli.transport, cli.proto, cli.addr)
-
-		configFile, e := cliconfig.Load(cliconfig.ConfigDir())
-		if e != nil {
-			fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
-		}
-		cli.configFile = configFile
-
 		return nil
 	}
 
 	return cli
 }
+
+func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) {
+	switch len(hosts) {
+	case 0:
+		host = os.Getenv("DOCKER_HOST")
+	case 1:
+		host = hosts[0]
+	default:
+		return "", errors.New("Please specify only one -H")
+	}
+
+	defaultHost := opts.DefaultTCPHost
+	if tlsOptions != nil {
+		defaultHost = opts.DefaultTLSHost
+	}
+
+	host, err = opts.ParseHost(defaultHost, host)
+	return
+}

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

@@ -0,0 +1,98 @@
+package lib
+
+import (
+	"crypto/tls"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/docker/docker/api"
+	"github.com/docker/docker/pkg/sockets"
+	"github.com/docker/docker/pkg/tlsconfig"
+	"github.com/docker/docker/pkg/version"
+)
+
+// Client is the API client that performs all operations
+// against a docker server.
+type Client struct {
+	// proto holds the client protocol i.e. unix.
+	Proto string
+	// addr holds the client address.
+	Addr string
+	// basePath holds the path to prepend to the requests
+	BasePath string
+	// scheme holds the scheme of the client i.e. https.
+	Scheme string
+	// httpClient holds the client transport instance. Exported to keep the old code running.
+	HTTPClient *http.Client
+	// version of the server to talk to.
+	version version.Version
+	// custom http headers configured by users
+	customHTTPHeaders map[string]string
+}
+
+// NewClient initializes a new API client
+// for the given host. It uses the tlsOptions
+// to decide whether to use a secure connection or not.
+// It also initializes the custom http headers to add to each request.
+func NewClient(host string, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) {
+	return NewClientWithVersion(host, api.Version, tlsOptions, httpHeaders)
+}
+
+// NewClientWithVersion initializes a new API client
+// for the given host and API version. It uses the tlsOptions
+// to decide whether to use a secure connection or not.
+// It also initializes the custom http headers to add to each request.
+func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) {
+	var (
+		basePath       string
+		tlsConfig      *tls.Config
+		scheme         = "http"
+		protoAddrParts = strings.SplitN(host, "://", 2)
+		proto, addr    = protoAddrParts[0], protoAddrParts[1]
+	)
+
+	if proto == "tcp" {
+		parsed, err := url.Parse("tcp://" + addr)
+		if err != nil {
+			return nil, err
+		}
+		addr = parsed.Host
+		basePath = parsed.Path
+	}
+
+	if tlsOptions != nil {
+		scheme = "https"
+		var err error
+		tlsConfig, err = tlsconfig.Client(*tlsOptions)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// The transport is created here for reuse during the client session.
+	transport := &http.Transport{
+		TLSClientConfig: tlsConfig,
+	}
+	sockets.ConfigureTCPTransport(transport, proto, addr)
+
+	return &Client{
+		Addr:              addr,
+		BasePath:          basePath,
+		Scheme:            scheme,
+		HTTPClient:        &http.Client{Transport: transport},
+		version:           version,
+		customHTTPHeaders: httpHeaders,
+	}, nil
+}
+
+// getAPIPath returns the versioned request path to call the api.
+// It appends the query parameters to the path if they are not empty.
+func (cli *Client) getAPIPath(p string, query url.Values) string {
+	apiPath := fmt.Sprintf("%s/v%s%s", cli.BasePath, cli.version, p)
+	if len(query) > 0 {
+		apiPath += "?" + query.Encode()
+	}
+	return apiPath
+}

+ 155 - 0
api/client/lib/request.go

@@ -0,0 +1,155 @@
+package lib
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/docker/docker/utils"
+)
+
+// ServerResponse is a wrapper for http API responses.
+type ServerResponse struct {
+	body       io.ReadCloser
+	header     http.Header
+	statusCode int
+}
+
+// HEAD sends an http request to the docker API using the method HEAD.
+func (cli *Client) HEAD(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
+	return cli.sendRequest("HEAD", path, query, nil, headers)
+}
+
+// GET sends an http request to the docker API using the method GET.
+func (cli *Client) GET(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
+	return cli.sendRequest("GET", path, query, nil, headers)
+}
+
+// POST sends an http request to the docker API using the method POST.
+func (cli *Client) POST(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
+	return cli.sendRequest("POST", path, query, body, headers)
+}
+
+// POSTRaw sends the raw input to the docker API using the method POST.
+func (cli *Client) POSTRaw(path string, query url.Values, body io.Reader, headers map[string][]string) (*ServerResponse, error) {
+	return cli.sendClientRequest("POST", path, query, body, headers)
+}
+
+// PUT sends an http request to the docker API using the method PUT.
+func (cli *Client) PUT(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
+	return cli.sendRequest("PUT", path, query, body, headers)
+}
+
+// DELETE sends an http request to the docker API using the method DELETE.
+func (cli *Client) DELETE(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
+	return cli.sendRequest("DELETE", path, query, nil, headers)
+}
+
+func (cli *Client) sendRequest(method, path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
+	params, err := encodeData(body)
+	if err != nil {
+		return nil, err
+	}
+
+	if body != nil {
+		if headers == nil {
+			headers = make(map[string][]string)
+		}
+		headers["Content-Type"] = []string{"application/json"}
+	}
+
+	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) {
+	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)
+	}
+
+	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")
+	}
+
+	resp, err := cli.HTTPClient.Do(req)
+	if resp != nil {
+		serverResp.statusCode = resp.StatusCode
+	}
+
+	if err != nil {
+		if utils.IsTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") {
+			return serverResp, errConnectionFailed
+		}
+
+		if cli.Scheme == "http" && strings.Contains(err.Error(), "malformed HTTP response") {
+			return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)
+		}
+		if cli.Scheme == "https" && strings.Contains(err.Error(), "remote error: bad certificate") {
+			return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err)
+		}
+
+		return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err)
+	}
+
+	if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
+		body, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			return serverResp, err
+		}
+		if len(body) == 0 {
+			return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL)
+		}
+		return serverResp, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
+	}
+
+	serverResp.body = resp.Body
+	serverResp.header = resp.Header
+	return serverResp, nil
+}
+
+func encodeData(data interface{}) (*bytes.Buffer, error) {
+	params := bytes.NewBuffer(nil)
+	if data != nil {
+		if err := json.NewEncoder(params).Encode(data); err != nil {
+			return nil, err
+		}
+	}
+	return params, nil
+}
+
+func ensureReaderClosed(response *ServerResponse) {
+	if response != nil && response.body != nil {
+		response.body.Close()
+	}
+}

+ 8 - 1
cliconfig/config.go

@@ -192,7 +192,14 @@ func Load(configDir string) (*ConfigFile, error) {
 	}
 	defer file.Close()
 	err = configFile.LegacyLoadFromReader(file)
-	return &configFile, err
+	if err != nil {
+		return &configFile, err
+	}
+
+	if configFile.HTTPHeaders == nil {
+		configFile.HTTPHeaders = map[string]string{}
+	}
+	return &configFile, nil
 }
 
 // SaveToWriter encodes and writes out all the authorization information to