Bladeren bron

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 6 jaren geleden
bovenliggende
commit
b26aa97914
5 gewijzigde bestanden met toevoegingen van 84 en 10 verwijderingen
  1. 37 7
      client/client.go
  2. 34 1
      client/client_test.go
  3. 1 1
      client/hijack.go
  4. 11 0
      client/options.go
  5. 1 1
      client/request.go

+ 37 - 7
client/client.go

@@ -81,6 +81,15 @@ type Client struct {
 	customHTTPHeaders map[string]string
 	customHTTPHeaders map[string]string
 	// manualOverride is set to true when the version was set by users.
 	// manualOverride is set to true when the version was set by users.
 	manualOverride bool
 	manualOverride bool
+
+	// negotiateVersion indicates if the client should automatically negotiate
+	// the API version to use when making requests. API version negotiation is
+	// performed on the first request, after which negotiated is set to "true"
+	// so that subsequent requests do not re-negotiate.
+	negotiateVersion bool
+
+	// negotiated indicates that API version negotiation took place
+	negotiated bool
 }
 }
 
 
 // CheckRedirect specifies the policy for dealing with redirect responses:
 // CheckRedirect specifies the policy for dealing with redirect responses:
@@ -169,8 +178,11 @@ func (cli *Client) Close() error {
 
 
 // getAPIPath returns the versioned request path to call the api.
 // getAPIPath returns the versioned request path to call the api.
 // It appends the query parameters to the path if they are not empty.
 // It appends the query parameters to the path if they are not empty.
-func (cli *Client) getAPIPath(p string, query url.Values) string {
+func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
 	var apiPath string
 	var apiPath string
+	if cli.negotiateVersion && !cli.negotiated {
+		cli.NegotiateAPIVersion(ctx)
+	}
 	if cli.version != "" {
 	if cli.version != "" {
 		v := strings.TrimPrefix(cli.version, "v")
 		v := strings.TrimPrefix(cli.version, "v")
 		apiPath = path.Join(cli.basePath, "/v"+v, p)
 		apiPath = path.Join(cli.basePath, "/v"+v, p)
@@ -186,19 +198,31 @@ func (cli *Client) ClientVersion() string {
 }
 }
 
 
 // NegotiateAPIVersion queries the API and updates the version to match the
 // NegotiateAPIVersion queries the API and updates the version to match the
-// API version. Any errors are silently ignored.
+// API version. Any errors are silently ignored. If a manual override is in place,
+// either through the `DOCKER_API_VERSION` environment variable, or if the client
+// was initialized with a fixed version (`opts.WithVersion(xx)`), no negotiation
+// will be performed.
 func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
 func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
-	ping, _ := cli.Ping(ctx)
-	cli.NegotiateAPIVersionPing(ping)
+	if !cli.manualOverride {
+		ping, _ := cli.Ping(ctx)
+		cli.negotiateAPIVersionPing(ping)
+	}
 }
 }
 
 
 // NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion
 // NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion
-// if the ping version is less than the default version.
+// if the ping version is less than the default version.  If a manual override is
+// in place, either through the `DOCKER_API_VERSION` environment variable, or if
+// the client was initialized with a fixed version (`opts.WithVersion(xx)`), no
+// negotiation is performed.
 func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
 func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
-	if cli.manualOverride {
-		return
+	if !cli.manualOverride {
+		cli.negotiateAPIVersionPing(p)
 	}
 	}
+}
 
 
+// negotiateAPIVersionPing queries the API and updates the version to match the
+// API version. Any errors are silently ignored.
+func (cli *Client) negotiateAPIVersionPing(p types.Ping) {
 	// try the latest version before versioning headers existed
 	// try the latest version before versioning headers existed
 	if p.APIVersion == "" {
 	if p.APIVersion == "" {
 		p.APIVersion = "1.24"
 		p.APIVersion = "1.24"
@@ -213,6 +237,12 @@ func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
 	if versions.LessThan(p.APIVersion, cli.version) {
 	if versions.LessThan(p.APIVersion, cli.version) {
 		cli.version = p.APIVersion
 		cli.version = p.APIVersion
 	}
 	}
+
+	// Store the results, so that automatic API version negotiation (if enabled)
+	// won't be performed on the next request.
+	if cli.negotiateVersion {
+		cli.negotiated = true
+	}
 }
 }
 
 
 // DaemonHost returns the host address used by the client
 // DaemonHost returns the host address used by the client

+ 34 - 1
client/client_test.go

@@ -2,10 +2,13 @@ package client // import "github.com/docker/docker/client"
 
 
 import (
 import (
 	"bytes"
 	"bytes"
+	"context"
+	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"runtime"
 	"runtime"
+	"strings"
 	"testing"
 	"testing"
 
 
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api"
@@ -123,9 +126,10 @@ func TestGetAPIPath(t *testing.T) {
 		{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
 		{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
 	}
 	}
 
 
+	ctx := context.TODO()
 	for _, testcase := range testcases {
 	for _, testcase := range testcases {
 		c := Client{version: testcase.version, basePath: "/"}
 		c := Client{version: testcase.version, basePath: "/"}
-		actual := c.getAPIPath(testcase.path, testcase.query)
+		actual := c.getAPIPath(ctx, testcase.path, testcase.query)
 		assert.Check(t, is.Equal(actual, testcase.expected))
 		assert.Check(t, is.Equal(actual, testcase.expected))
 	}
 	}
 }
 }
@@ -265,6 +269,35 @@ func TestNegotiateAPVersionOverride(t *testing.T) {
 	assert.Check(t, is.Equal(expected, client.version))
 	assert.Check(t, is.Equal(expected, client.version))
 }
 }
 
 
+func TestNegotiateAPIVersionAutomatic(t *testing.T) {
+	var pingVersion string
+	httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
+		resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}}
+		resp.Header.Set("API-Version", pingVersion)
+		resp.Body = ioutil.NopCloser(strings.NewReader("OK"))
+		return resp, nil
+	})
+
+	client, err := NewClientWithOpts(
+		WithHTTPClient(httpClient),
+		WithAPIVersionNegotiation(),
+	)
+	assert.NilError(t, err)
+
+	ctx := context.Background()
+	assert.Equal(t, client.ClientVersion(), api.DefaultVersion)
+
+	// First request should trigger negotiation
+	pingVersion = "1.35"
+	_, _ = client.Info(ctx)
+	assert.Equal(t, client.ClientVersion(), "1.35")
+
+	// Once successfully negotiated, subsequent requests should not re-negotiate
+	pingVersion = "1.25"
+	_, _ = client.Info(ctx)
+	assert.Equal(t, client.ClientVersion(), "1.35")
+}
+
 // TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
 // TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
 // with an empty version string does still allow API-version negotiation
 // with an empty version string does still allow API-version negotiation
 func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
 func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {

+ 1 - 1
client/hijack.go

@@ -23,7 +23,7 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu
 		return types.HijackedResponse{}, err
 		return types.HijackedResponse{}, err
 	}
 	}
 
 
-	apiPath := cli.getAPIPath(path, query)
+	apiPath := cli.getAPIPath(ctx, path, query)
 	req, err := http.NewRequest("POST", apiPath, bodyEncoded)
 	req, err := http.NewRequest("POST", apiPath, bodyEncoded)
 	if err != nil {
 	if err != nil {
 		return types.HijackedResponse{}, err
 		return types.HijackedResponse{}, err

+ 11 - 0
client/options.go

@@ -150,3 +150,14 @@ func WithVersion(version string) Opt {
 		return nil
 		return nil
 	}
 	}
 }
 }
+
+// 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.
+func WithAPIVersionNegotiation() Opt {
+	return func(c *Client) error {
+		c.negotiateVersion = true
+		return nil
+	}
+}

+ 1 - 1
client/request.go

@@ -115,7 +115,7 @@ func (cli *Client) buildRequest(method, path string, body io.Reader, headers hea
 }
 }
 
 
 func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) {
 func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) {
-	req, err := cli.buildRequest(method, cli.getAPIPath(path, query), body, headers)
+	req, err := cli.buildRequest(method, cli.getAPIPath(ctx, path, query), body, headers)
 	if err != nil {
 	if err != nil {
 		return serverResponse{}, err
 		return serverResponse{}, err
 	}
 	}