Add client.WithAPIVersionNegotiation() option
| ... | ... |
@@ -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 |
} |