From 41da5700a4346133d19f78b95e2f15ac59e9d25a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 17 Jul 2025 11:20:17 +0200 Subject: [PATCH] client: define default (and maximum) API version With the client and API migrating to separate modules, users of the Client module may upgrade the API module to higher versions. Currently, the Client uses the API's Default version. While the version of the API module is allowed to be updated (following SemVer), we should not allow the Client to support higher API versions than it was written for. This patch introduces a DefaultAPIVersion in the client package that is used as default version of the API for the client to use. Signed-off-by: Sebastiaan van Stijn --- client/client.go | 15 ++++++++++++--- client/client_test.go | 27 +++++++++++++-------------- client/options.go | 8 ++++++++ client/options_test.go | 3 +-- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/client/client.go b/client/client.go index 8acfb7f490..298852f55a 100644 --- a/client/client.go +++ b/client/client.go @@ -53,7 +53,6 @@ import ( "sync/atomic" "time" - "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" "github.com/docker/go-connections/sockets" @@ -91,6 +90,16 @@ import ( // [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569 const DummyHost = "api.moby.localhost" +// DefaultAPIVersion is the highest REST API version supported by the client. +// If API-version negotiation is enabled (see [WithAPIVersionNegotiation], +// [Client.NegotiateAPIVersion]), the client may downgrade its API version. +// Similarly, the [WithVersion] and [WithVersionFromEnv] allow overriding +// the version. +// +// This version may be lower than the [api.DefaultVersion], which is the default +// (and highest supported) version of the api library module used. +const DefaultAPIVersion = "1.52" + // fallbackAPIVersion is the version to fallback to if API-version negotiation // fails. This version is the highest version of the API before API-version // negotiation was introduced. If negotiation fails (or no API version was @@ -198,7 +207,7 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) { } c := &Client{ host: DefaultDockerHost, - version: api.DefaultVersion, + version: DefaultAPIVersion, client: client, proto: hostURL.Scheme, addr: hostURL.Host, @@ -381,7 +390,7 @@ func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) { // if the client is not initialized with a version, start with the latest supported version if cli.version == "" { - cli.version = api.DefaultVersion + cli.version = DefaultAPIVersion } // if server version is lower than the client version, downgrade diff --git a/client/client_test.go b/client/client_test.go index 8958d38a6c..6f2085f5b5 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -11,7 +11,6 @@ import ( "strings" "testing" - "github.com/docker/docker/api" "github.com/docker/docker/api/types" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -30,7 +29,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) { { doc: "default api version", envs: map[string]string{}, - expectedVersion: api.DefaultVersion, + expectedVersion: DefaultAPIVersion, }, { doc: "invalid cert path", @@ -44,7 +43,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) { envs: map[string]string{ "DOCKER_CERT_PATH": "testdata/", }, - expectedVersion: api.DefaultVersion, + expectedVersion: DefaultAPIVersion, }, { doc: "default api version with cert path and tls verify", @@ -52,7 +51,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) { "DOCKER_CERT_PATH": "testdata/", "DOCKER_TLS_VERIFY": "1", }, - expectedVersion: api.DefaultVersion, + expectedVersion: DefaultAPIVersion, }, { doc: "default api version with cert path and host", @@ -60,7 +59,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) { "DOCKER_CERT_PATH": "testdata/", "DOCKER_HOST": "https://notaunixsocket", }, - expectedVersion: api.DefaultVersion, + expectedVersion: DefaultAPIVersion, }, { doc: "invalid docker host", @@ -74,7 +73,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) { envs: map[string]string{ "DOCKER_HOST": "invalid://url", }, - expectedVersion: api.DefaultVersion, + expectedVersion: DefaultAPIVersion, }, { doc: "override api version", @@ -117,17 +116,17 @@ func TestGetAPIPath(t *testing.T) { }{ { path: "/containers/json", - expected: "/v" + api.DefaultVersion + "/containers/json", + expected: "/v" + DefaultAPIVersion + "/containers/json", }, { path: "/containers/json", query: url.Values{}, - expected: "/v" + api.DefaultVersion + "/containers/json", + expected: "/v" + DefaultAPIVersion + "/containers/json", }, { path: "/containers/json", query: url.Values{"s": []string{"c"}}, - expected: "/v" + api.DefaultVersion + "/containers/json?s=c", + expected: "/v" + DefaultAPIVersion + "/containers/json?s=c", }, { version: "1.22", @@ -235,7 +234,7 @@ func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) { client, err := NewClientWithOpts(FromEnv) assert.NilError(t, err) - assert.Check(t, is.Equal(client.ClientVersion(), api.DefaultVersion)) + assert.Check(t, is.Equal(client.ClientVersion(), DefaultAPIVersion)) const expected = "1.22" t.Setenv("DOCKER_API_VERSION", expected) @@ -375,8 +374,8 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) { ) assert.NilError(t, err) - // Client defaults to use api.DefaultVersion before version-negotiation. - expected := api.DefaultVersion + // Client defaults to use DefaultAPIVersion before version-negotiation. + expected := DefaultAPIVersion assert.Check(t, is.Equal(client.ClientVersion(), expected)) // First request should trigger negotiation @@ -423,7 +422,7 @@ func TestCustomAPIVersion(t *testing.T) { }{ { version: "", - expected: api.DefaultVersion, + expected: DefaultAPIVersion, }, { version: "1.0", @@ -435,7 +434,7 @@ func TestCustomAPIVersion(t *testing.T) { }, { version: "v", - expected: api.DefaultVersion, + expected: DefaultAPIVersion, }, { version: "v1.0", diff --git a/client/options.go b/client/options.go index 6f68fc2b89..320989ac60 100644 --- a/client/options.go +++ b/client/options.go @@ -194,6 +194,10 @@ func WithTLSClientConfigFromEnv() Opt { // WithVersion overrides the client version with the specified one. If an empty // version is provided, the value is ignored to allow version negotiation // (see [WithAPIVersionNegotiation]). +// +// WithVersion does not validate if the client supports the given version, +// and callers should verify if the version is in the correct format and +// lower than the maximum supported version as defined by [DefaultAPIVersion]. func WithVersion(version string) Opt { return func(c *Client) error { if v := strings.TrimPrefix(version, "v"); v != "" { @@ -208,6 +212,10 @@ func WithVersion(version string) Opt { // the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable. // If DOCKER_API_VERSION is not set, or set to an empty value, the version // is not modified. +// +// WithVersion does not validate if the client supports the given version, +// and callers should verify if the version is in the correct format and +// lower than the maximum supported version as defined by [DefaultAPIVersion]. func WithVersionFromEnv() Opt { return func(c *Client) error { return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c) diff --git a/client/options_test.go b/client/options_test.go index 7f4b1a0d9a..c465093355 100644 --- a/client/options_test.go +++ b/client/options_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/docker/docker/api" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -50,7 +49,7 @@ func TestOptionWithVersionFromEnv(t *testing.T) { c, err := NewClientWithOpts(WithVersionFromEnv()) assert.NilError(t, err) assert.Check(t, c.client != nil) - assert.Check(t, is.Equal(c.version, api.DefaultVersion)) + assert.Check(t, is.Equal(c.version, DefaultAPIVersion)) assert.Check(t, is.Equal(c.manualOverride, false)) t.Setenv("DOCKER_API_VERSION", "2.9999")