Browse code

Extract API client struct as standalone client.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2015/12/03 13:53:06
Showing 4 changed files
... ...
@@ -6,14 +6,14 @@ import (
6 6
 	"fmt"
7 7
 	"io"
8 8
 	"net/http"
9
-	"net/url"
10 9
 	"os"
11
-	"strings"
10
+	"runtime"
12 11
 
12
+	"github.com/docker/docker/api/client/lib"
13 13
 	"github.com/docker/docker/cli"
14 14
 	"github.com/docker/docker/cliconfig"
15
+	"github.com/docker/docker/dockerversion"
15 16
 	"github.com/docker/docker/opts"
16
-	"github.com/docker/docker/pkg/sockets"
17 17
 	"github.com/docker/docker/pkg/term"
18 18
 	"github.com/docker/docker/pkg/tlsconfig"
19 19
 )
... ...
@@ -24,13 +24,6 @@ type DockerCli struct {
24 24
 	// initializing closure
25 25
 	init func() error
26 26
 
27
-	// proto holds the client protocol i.e. unix.
28
-	proto string
29
-	// addr holds the client address.
30
-	addr string
31
-	// basePath holds the path to prepend to the requests
32
-	basePath string
33
-
34 27
 	// configFile has the client configuration file
35 28
 	configFile *cliconfig.ConfigFile
36 29
 	// in holds the input stream and closer (io.ReadCloser) for the client.
... ...
@@ -41,11 +34,6 @@ type DockerCli struct {
41 41
 	err io.Writer
42 42
 	// keyFile holds the key file as a string.
43 43
 	keyFile string
44
-	// tlsConfig holds the TLS configuration for the client, and will
45
-	// set the scheme to https in NewDockerCli if present.
46
-	tlsConfig *tls.Config
47
-	// scheme holds the scheme of the client i.e. https.
48
-	scheme string
49 44
 	// inFd holds the file descriptor of the client's STDIN (if valid).
50 45
 	inFd uintptr
51 46
 	// outFd holds file descriptor of the client's STDOUT (if valid).
... ...
@@ -54,6 +42,22 @@ type DockerCli struct {
54 54
 	isTerminalIn bool
55 55
 	// isTerminalOut indicates whether the client's STDOUT is a TTY
56 56
 	isTerminalOut bool
57
+	// client is the http client that performs all API operations
58
+	client *lib.Client
59
+
60
+	// DEPRECATED OPTIONS TO MAKE THE CLIENT COMPILE
61
+	// TODO: Remove
62
+	// proto holds the client protocol i.e. unix.
63
+	proto string
64
+	// addr holds the client address.
65
+	addr string
66
+	// basePath holds the path to prepend to the requests
67
+	basePath string
68
+	// tlsConfig holds the TLS configuration for the client, and will
69
+	// set the scheme to https in NewDockerCli if present.
70
+	tlsConfig *tls.Config
71
+	// scheme holds the scheme of the client i.e. https.
72
+	scheme string
57 73
 	// transport holds the client transport instance.
58 74
 	transport *http.Transport
59 75
 }
... ...
@@ -98,50 +102,35 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
98 98
 	}
99 99
 
100 100
 	cli.init = func() error {
101
-
102 101
 		clientFlags.PostParse()
103
-
104
-		hosts := clientFlags.Common.Hosts
105
-
106
-		switch len(hosts) {
107
-		case 0:
108
-			hosts = []string{os.Getenv("DOCKER_HOST")}
109
-		case 1:
110
-			// only accept one host to talk to
111
-		default:
112
-			return errors.New("Please specify only one -H")
102
+		configFile, e := cliconfig.Load(cliconfig.ConfigDir())
103
+		if e != nil {
104
+			fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
113 105
 		}
106
+		cli.configFile = configFile
114 107
 
115
-		defaultHost := opts.DefaultTCPHost
116
-		if clientFlags.Common.TLSOptions != nil {
117
-			defaultHost = opts.DefaultTLSHost
108
+		host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions)
109
+		if err != nil {
110
+			return err
118 111
 		}
119 112
 
120
-		var e error
121
-		if hosts[0], e = opts.ParseHost(defaultHost, hosts[0]); e != nil {
122
-			return e
113
+		customHeaders := cli.configFile.HTTPHeaders
114
+		if customHeaders == nil {
115
+			customHeaders = map[string]string{}
123 116
 		}
117
+		customHeaders["User-Agent"] = "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
124 118
 
125
-		protoAddrParts := strings.SplitN(hosts[0], "://", 2)
126
-		cli.proto, cli.addr = protoAddrParts[0], protoAddrParts[1]
127
-
128
-		if cli.proto == "tcp" {
129
-			// error is checked in pkg/parsers already
130
-			parsed, _ := url.Parse("tcp://" + cli.addr)
131
-			cli.addr = parsed.Host
132
-			cli.basePath = parsed.Path
119
+		client, err := lib.NewClient(host, clientFlags.Common.TLSOptions, customHeaders)
120
+		if err != nil {
121
+			return err
133 122
 		}
123
+		cli.client = client
134 124
 
135
-		if clientFlags.Common.TLSOptions != nil {
136
-			cli.scheme = "https"
137
-			var e error
138
-			cli.tlsConfig, e = tlsconfig.Client(*clientFlags.Common.TLSOptions)
139
-			if e != nil {
140
-				return e
141
-			}
142
-		} else {
143
-			cli.scheme = "http"
144
-		}
125
+		// FIXME: Deprecated, only to keep the old code running.
126
+		cli.transport = client.HTTPClient.Transport.(*http.Transport)
127
+		cli.basePath = client.BasePath
128
+		cli.addr = client.Addr
129
+		cli.scheme = client.Scheme
145 130
 
146 131
 		if cli.in != nil {
147 132
 			cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in)
... ...
@@ -150,20 +139,27 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
150 150
 			cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out)
151 151
 		}
152 152
 
153
-		// The transport is created here for reuse during the client session.
154
-		cli.transport = &http.Transport{
155
-			TLSClientConfig: cli.tlsConfig,
156
-		}
157
-		sockets.ConfigureTCPTransport(cli.transport, cli.proto, cli.addr)
158
-
159
-		configFile, e := cliconfig.Load(cliconfig.ConfigDir())
160
-		if e != nil {
161
-			fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
162
-		}
163
-		cli.configFile = configFile
164
-
165 153
 		return nil
166 154
 	}
167 155
 
168 156
 	return cli
169 157
 }
158
+
159
+func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) {
160
+	switch len(hosts) {
161
+	case 0:
162
+		host = os.Getenv("DOCKER_HOST")
163
+	case 1:
164
+		host = hosts[0]
165
+	default:
166
+		return "", errors.New("Please specify only one -H")
167
+	}
168
+
169
+	defaultHost := opts.DefaultTCPHost
170
+	if tlsOptions != nil {
171
+		defaultHost = opts.DefaultTLSHost
172
+	}
173
+
174
+	host, err = opts.ParseHost(defaultHost, host)
175
+	return
176
+}
170 177
new file mode 100644
... ...
@@ -0,0 +1,98 @@
0
+package lib
1
+
2
+import (
3
+	"crypto/tls"
4
+	"fmt"
5
+	"net/http"
6
+	"net/url"
7
+	"strings"
8
+
9
+	"github.com/docker/docker/api"
10
+	"github.com/docker/docker/pkg/sockets"
11
+	"github.com/docker/docker/pkg/tlsconfig"
12
+	"github.com/docker/docker/pkg/version"
13
+)
14
+
15
+// Client is the API client that performs all operations
16
+// against a docker server.
17
+type Client struct {
18
+	// proto holds the client protocol i.e. unix.
19
+	Proto string
20
+	// addr holds the client address.
21
+	Addr string
22
+	// basePath holds the path to prepend to the requests
23
+	BasePath string
24
+	// scheme holds the scheme of the client i.e. https.
25
+	Scheme string
26
+	// httpClient holds the client transport instance. Exported to keep the old code running.
27
+	HTTPClient *http.Client
28
+	// version of the server to talk to.
29
+	version version.Version
30
+	// custom http headers configured by users
31
+	customHTTPHeaders map[string]string
32
+}
33
+
34
+// NewClient initializes a new API client
35
+// for the given host. It uses the tlsOptions
36
+// to decide whether to use a secure connection or not.
37
+// It also initializes the custom http headers to add to each request.
38
+func NewClient(host string, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) {
39
+	return NewClientWithVersion(host, api.Version, tlsOptions, httpHeaders)
40
+}
41
+
42
+// NewClientWithVersion initializes a new API client
43
+// for the given host and API version. It uses the tlsOptions
44
+// to decide whether to use a secure connection or not.
45
+// It also initializes the custom http headers to add to each request.
46
+func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) {
47
+	var (
48
+		basePath       string
49
+		tlsConfig      *tls.Config
50
+		scheme         = "http"
51
+		protoAddrParts = strings.SplitN(host, "://", 2)
52
+		proto, addr    = protoAddrParts[0], protoAddrParts[1]
53
+	)
54
+
55
+	if proto == "tcp" {
56
+		parsed, err := url.Parse("tcp://" + addr)
57
+		if err != nil {
58
+			return nil, err
59
+		}
60
+		addr = parsed.Host
61
+		basePath = parsed.Path
62
+	}
63
+
64
+	if tlsOptions != nil {
65
+		scheme = "https"
66
+		var err error
67
+		tlsConfig, err = tlsconfig.Client(*tlsOptions)
68
+		if err != nil {
69
+			return nil, err
70
+		}
71
+	}
72
+
73
+	// The transport is created here for reuse during the client session.
74
+	transport := &http.Transport{
75
+		TLSClientConfig: tlsConfig,
76
+	}
77
+	sockets.ConfigureTCPTransport(transport, proto, addr)
78
+
79
+	return &Client{
80
+		Addr:              addr,
81
+		BasePath:          basePath,
82
+		Scheme:            scheme,
83
+		HTTPClient:        &http.Client{Transport: transport},
84
+		version:           version,
85
+		customHTTPHeaders: httpHeaders,
86
+	}, nil
87
+}
88
+
89
+// getAPIPath returns the versioned request path to call the api.
90
+// It appends the query parameters to the path if they are not empty.
91
+func (cli *Client) getAPIPath(p string, query url.Values) string {
92
+	apiPath := fmt.Sprintf("%s/v%s%s", cli.BasePath, cli.version, p)
93
+	if len(query) > 0 {
94
+		apiPath += "?" + query.Encode()
95
+	}
96
+	return apiPath
97
+}
0 98
new file mode 100644
... ...
@@ -0,0 +1,155 @@
0
+package lib
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"io/ioutil"
8
+	"net/http"
9
+	"net/url"
10
+	"strings"
11
+
12
+	"github.com/docker/docker/utils"
13
+)
14
+
15
+// ServerResponse is a wrapper for http API responses.
16
+type ServerResponse struct {
17
+	body       io.ReadCloser
18
+	header     http.Header
19
+	statusCode int
20
+}
21
+
22
+// HEAD sends an http request to the docker API using the method HEAD.
23
+func (cli *Client) HEAD(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
24
+	return cli.sendRequest("HEAD", path, query, nil, headers)
25
+}
26
+
27
+// GET sends an http request to the docker API using the method GET.
28
+func (cli *Client) GET(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
29
+	return cli.sendRequest("GET", path, query, nil, headers)
30
+}
31
+
32
+// POST sends an http request to the docker API using the method POST.
33
+func (cli *Client) POST(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
34
+	return cli.sendRequest("POST", path, query, body, headers)
35
+}
36
+
37
+// POSTRaw sends the raw input to the docker API using the method POST.
38
+func (cli *Client) POSTRaw(path string, query url.Values, body io.Reader, headers map[string][]string) (*ServerResponse, error) {
39
+	return cli.sendClientRequest("POST", path, query, body, headers)
40
+}
41
+
42
+// PUT sends an http request to the docker API using the method PUT.
43
+func (cli *Client) PUT(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
44
+	return cli.sendRequest("PUT", path, query, body, headers)
45
+}
46
+
47
+// DELETE sends an http request to the docker API using the method DELETE.
48
+func (cli *Client) DELETE(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
49
+	return cli.sendRequest("DELETE", path, query, nil, headers)
50
+}
51
+
52
+func (cli *Client) sendRequest(method, path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
53
+	params, err := encodeData(body)
54
+	if err != nil {
55
+		return nil, err
56
+	}
57
+
58
+	if body != nil {
59
+		if headers == nil {
60
+			headers = make(map[string][]string)
61
+		}
62
+		headers["Content-Type"] = []string{"application/json"}
63
+	}
64
+
65
+	return cli.sendClientRequest(method, path, query, params, headers)
66
+}
67
+
68
+func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*ServerResponse, error) {
69
+	serverResp := &ServerResponse{
70
+		body:       nil,
71
+		statusCode: -1,
72
+	}
73
+
74
+	expectedPayload := (method == "POST" || method == "PUT")
75
+	if expectedPayload && in == nil {
76
+		in = bytes.NewReader([]byte{})
77
+	}
78
+
79
+	apiPath := cli.getAPIPath(path, query)
80
+	req, err := http.NewRequest(method, apiPath, in)
81
+	if err != nil {
82
+		return serverResp, err
83
+	}
84
+
85
+	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
86
+	// then the user can't change OUR headers
87
+	for k, v := range cli.customHTTPHeaders {
88
+		req.Header.Set(k, v)
89
+	}
90
+
91
+	req.URL.Host = cli.Addr
92
+	req.URL.Scheme = cli.Scheme
93
+
94
+	if headers != nil {
95
+		for k, v := range headers {
96
+			req.Header[k] = v
97
+		}
98
+	}
99
+
100
+	if expectedPayload && req.Header.Get("Content-Type") == "" {
101
+		req.Header.Set("Content-Type", "text/plain")
102
+	}
103
+
104
+	resp, err := cli.HTTPClient.Do(req)
105
+	if resp != nil {
106
+		serverResp.statusCode = resp.StatusCode
107
+	}
108
+
109
+	if err != nil {
110
+		if utils.IsTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") {
111
+			return serverResp, errConnectionFailed
112
+		}
113
+
114
+		if cli.Scheme == "http" && strings.Contains(err.Error(), "malformed HTTP response") {
115
+			return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)
116
+		}
117
+		if cli.Scheme == "https" && strings.Contains(err.Error(), "remote error: bad certificate") {
118
+			return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err)
119
+		}
120
+
121
+		return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err)
122
+	}
123
+
124
+	if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
125
+		body, err := ioutil.ReadAll(resp.Body)
126
+		if err != nil {
127
+			return serverResp, err
128
+		}
129
+		if len(body) == 0 {
130
+			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)
131
+		}
132
+		return serverResp, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
133
+	}
134
+
135
+	serverResp.body = resp.Body
136
+	serverResp.header = resp.Header
137
+	return serverResp, nil
138
+}
139
+
140
+func encodeData(data interface{}) (*bytes.Buffer, error) {
141
+	params := bytes.NewBuffer(nil)
142
+	if data != nil {
143
+		if err := json.NewEncoder(params).Encode(data); err != nil {
144
+			return nil, err
145
+		}
146
+	}
147
+	return params, nil
148
+}
149
+
150
+func ensureReaderClosed(response *ServerResponse) {
151
+	if response != nil && response.body != nil {
152
+		response.body.Close()
153
+	}
154
+}
... ...
@@ -192,7 +192,14 @@ func Load(configDir string) (*ConfigFile, error) {
192 192
 	}
193 193
 	defer file.Close()
194 194
 	err = configFile.LegacyLoadFromReader(file)
195
-	return &configFile, err
195
+	if err != nil {
196
+		return &configFile, err
197
+	}
198
+
199
+	if configFile.HTTPHeaders == nil {
200
+		configFile.HTTPHeaders = map[string]string{}
201
+	}
202
+	return &configFile, nil
196 203
 }
197 204
 
198 205
 // SaveToWriter encodes and writes out all the authorization information to