Browse code

Add client.WithAPIVersionNegotiation() option

WithAPIVersionNegotiation enables automatic API version negotiation for the client.

With this option enabled, the client automatically negotiates the API version
to use when making requests. API version negotiation is performed on the first
request; subsequent requests will not re-negotiate.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2019/04/08 21:14:07
Showing 5 changed files
... ...
@@ -81,6 +81,15 @@ type Client struct {
81 81
 	customHTTPHeaders map[string]string
82 82
 	// manualOverride is set to true when the version was set by users.
83 83
 	manualOverride bool
84
+
85
+	// negotiateVersion indicates if the client should automatically negotiate
86
+	// the API version to use when making requests. API version negotiation is
87
+	// performed on the first request, after which negotiated is set to "true"
88
+	// so that subsequent requests do not re-negotiate.
89
+	negotiateVersion bool
90
+
91
+	// negotiated indicates that API version negotiation took place
92
+	negotiated bool
84 93
 }
85 94
 
86 95
 // CheckRedirect specifies the policy for dealing with redirect responses:
... ...
@@ -169,8 +178,11 @@ func (cli *Client) Close() error {
169 169
 
170 170
 // getAPIPath returns the versioned request path to call the api.
171 171
 // It appends the query parameters to the path if they are not empty.
172
-func (cli *Client) getAPIPath(p string, query url.Values) string {
172
+func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
173 173
 	var apiPath string
174
+	if cli.negotiateVersion && !cli.negotiated {
175
+		cli.NegotiateAPIVersion(ctx)
176
+	}
174 177
 	if cli.version != "" {
175 178
 		v := strings.TrimPrefix(cli.version, "v")
176 179
 		apiPath = path.Join(cli.basePath, "/v"+v, p)
... ...
@@ -186,19 +198,31 @@ func (cli *Client) ClientVersion() string {
186 186
 }
187 187
 
188 188
 // NegotiateAPIVersion queries the API and updates the version to match the
189
-// API version. Any errors are silently ignored.
189
+// API version. Any errors are silently ignored. If a manual override is in place,
190
+// either through the `DOCKER_API_VERSION` environment variable, or if the client
191
+// was initialized with a fixed version (`opts.WithVersion(xx)`), no negotiation
192
+// will be performed.
190 193
 func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
191
-	ping, _ := cli.Ping(ctx)
192
-	cli.NegotiateAPIVersionPing(ping)
194
+	if !cli.manualOverride {
195
+		ping, _ := cli.Ping(ctx)
196
+		cli.negotiateAPIVersionPing(ping)
197
+	}
193 198
 }
194 199
 
195 200
 // NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion
196
-// if the ping version is less than the default version.
201
+// if the ping version is less than the default version.  If a manual override is
202
+// in place, either through the `DOCKER_API_VERSION` environment variable, or if
203
+// the client was initialized with a fixed version (`opts.WithVersion(xx)`), no
204
+// negotiation is performed.
197 205
 func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
198
-	if cli.manualOverride {
199
-		return
206
+	if !cli.manualOverride {
207
+		cli.negotiateAPIVersionPing(p)
200 208
 	}
209
+}
201 210
 
211
+// negotiateAPIVersionPing queries the API and updates the version to match the
212
+// API version. Any errors are silently ignored.
213
+func (cli *Client) negotiateAPIVersionPing(p types.Ping) {
202 214
 	// try the latest version before versioning headers existed
203 215
 	if p.APIVersion == "" {
204 216
 		p.APIVersion = "1.24"
... ...
@@ -213,6 +237,12 @@ func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
213 213
 	if versions.LessThan(p.APIVersion, cli.version) {
214 214
 		cli.version = p.APIVersion
215 215
 	}
216
+
217
+	// Store the results, so that automatic API version negotiation (if enabled)
218
+	// won't be performed on the next request.
219
+	if cli.negotiateVersion {
220
+		cli.negotiated = true
221
+	}
216 222
 }
217 223
 
218 224
 // DaemonHost returns the host address used by the client
... ...
@@ -2,10 +2,13 @@ package client // import "github.com/docker/docker/client"
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"context"
6
+	"io/ioutil"
5 7
 	"net/http"
6 8
 	"net/url"
7 9
 	"os"
8 10
 	"runtime"
11
+	"strings"
9 12
 	"testing"
10 13
 
11 14
 	"github.com/docker/docker/api"
... ...
@@ -123,9 +126,10 @@ func TestGetAPIPath(t *testing.T) {
123 123
 		{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
124 124
 	}
125 125
 
126
+	ctx := context.TODO()
126 127
 	for _, testcase := range testcases {
127 128
 		c := Client{version: testcase.version, basePath: "/"}
128
-		actual := c.getAPIPath(testcase.path, testcase.query)
129
+		actual := c.getAPIPath(ctx, testcase.path, testcase.query)
129 130
 		assert.Check(t, is.Equal(actual, testcase.expected))
130 131
 	}
131 132
 }
... ...
@@ -265,6 +269,35 @@ func TestNegotiateAPVersionOverride(t *testing.T) {
265 265
 	assert.Check(t, is.Equal(expected, client.version))
266 266
 }
267 267
 
268
+func TestNegotiateAPIVersionAutomatic(t *testing.T) {
269
+	var pingVersion string
270
+	httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
271
+		resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}}
272
+		resp.Header.Set("API-Version", pingVersion)
273
+		resp.Body = ioutil.NopCloser(strings.NewReader("OK"))
274
+		return resp, nil
275
+	})
276
+
277
+	client, err := NewClientWithOpts(
278
+		WithHTTPClient(httpClient),
279
+		WithAPIVersionNegotiation(),
280
+	)
281
+	assert.NilError(t, err)
282
+
283
+	ctx := context.Background()
284
+	assert.Equal(t, client.ClientVersion(), api.DefaultVersion)
285
+
286
+	// First request should trigger negotiation
287
+	pingVersion = "1.35"
288
+	_, _ = client.Info(ctx)
289
+	assert.Equal(t, client.ClientVersion(), "1.35")
290
+
291
+	// Once successfully negotiated, subsequent requests should not re-negotiate
292
+	pingVersion = "1.25"
293
+	_, _ = client.Info(ctx)
294
+	assert.Equal(t, client.ClientVersion(), "1.35")
295
+}
296
+
268 297
 // TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
269 298
 // with an empty version string does still allow API-version negotiation
270 299
 func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
... ...
@@ -23,7 +23,7 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu
23 23
 		return types.HijackedResponse{}, err
24 24
 	}
25 25
 
26
-	apiPath := cli.getAPIPath(path, query)
26
+	apiPath := cli.getAPIPath(ctx, path, query)
27 27
 	req, err := http.NewRequest("POST", apiPath, bodyEncoded)
28 28
 	if err != nil {
29 29
 		return types.HijackedResponse{}, err
... ...
@@ -150,3 +150,14 @@ func WithVersion(version string) Opt {
150 150
 		return nil
151 151
 	}
152 152
 }
153
+
154
+// WithAPIVersionNegotiation enables automatic API version negotiation for the client.
155
+// With this option enabled, the client automatically negotiates the API version
156
+// to use when making requests. API version negotiation is performed on the first
157
+// request; subsequent requests will not re-negotiate.
158
+func WithAPIVersionNegotiation() Opt {
159
+	return func(c *Client) error {
160
+		c.negotiateVersion = true
161
+		return nil
162
+	}
163
+}
... ...
@@ -115,7 +115,7 @@ func (cli *Client) buildRequest(method, path string, body io.Reader, headers hea
115 115
 }
116 116
 
117 117
 func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) {
118
-	req, err := cli.buildRequest(method, cli.getAPIPath(path, query), body, headers)
118
+	req, err := cli.buildRequest(method, cli.getAPIPath(ctx, path, query), body, headers)
119 119
 	if err != nil {
120 120
 		return serverResponse{}, err
121 121
 	}