Browse code

Merge pull request #39032 from thaJeztah/improve_version_negotiation

Add client.WithAPIVersionNegotiation() option

Sebastiaan van Stijn authored on 2019/04/20 20:34:22
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
... ...
@@ -159,3 +159,14 @@ func WithVersion(version string) Opt {
159 159
 		return nil
160 160
 	}
161 161
 }
162
+
163
+// WithAPIVersionNegotiation enables automatic API version negotiation for the client.
164
+// With this option enabled, the client automatically negotiates the API version
165
+// to use when making requests. API version negotiation is performed on the first
166
+// request; subsequent requests will not re-negotiate.
167
+func WithAPIVersionNegotiation() Opt {
168
+	return func(c *Client) error {
169
+		c.negotiateVersion = true
170
+		return nil
171
+	}
172
+}
... ...
@@ -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
 	}