Browse code

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 <github@gone.nl>

Sebastiaan van Stijn authored on 2025/07/17 18:20:17
Showing 4 changed files
... ...
@@ -53,7 +53,6 @@ import (
53 53
 	"sync/atomic"
54 54
 	"time"
55 55
 
56
-	"github.com/docker/docker/api"
57 56
 	"github.com/docker/docker/api/types"
58 57
 	"github.com/docker/docker/api/types/versions"
59 58
 	"github.com/docker/go-connections/sockets"
... ...
@@ -91,6 +90,16 @@ import (
91 91
 // [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569
92 92
 const DummyHost = "api.moby.localhost"
93 93
 
94
+// DefaultAPIVersion is the highest REST API version supported by the client.
95
+// If API-version negotiation is enabled (see [WithAPIVersionNegotiation],
96
+// [Client.NegotiateAPIVersion]), the client may downgrade its API version.
97
+// Similarly, the [WithVersion] and [WithVersionFromEnv] allow overriding
98
+// the version.
99
+//
100
+// This version may be lower than the [api.DefaultVersion], which is the default
101
+// (and highest supported) version of the api library module used.
102
+const DefaultAPIVersion = "1.52"
103
+
94 104
 // fallbackAPIVersion is the version to fallback to if API-version negotiation
95 105
 // fails. This version is the highest version of the API before API-version
96 106
 // negotiation was introduced. If negotiation fails (or no API version was
... ...
@@ -198,7 +207,7 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) {
198 198
 	}
199 199
 	c := &Client{
200 200
 		host:    DefaultDockerHost,
201
-		version: api.DefaultVersion,
201
+		version: DefaultAPIVersion,
202 202
 		client:  client,
203 203
 		proto:   hostURL.Scheme,
204 204
 		addr:    hostURL.Host,
... ...
@@ -381,7 +390,7 @@ func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) {
381 381
 
382 382
 	// if the client is not initialized with a version, start with the latest supported version
383 383
 	if cli.version == "" {
384
-		cli.version = api.DefaultVersion
384
+		cli.version = DefaultAPIVersion
385 385
 	}
386 386
 
387 387
 	// if server version is lower than the client version, downgrade
... ...
@@ -11,7 +11,6 @@ import (
11 11
 	"strings"
12 12
 	"testing"
13 13
 
14
-	"github.com/docker/docker/api"
15 14
 	"github.com/docker/docker/api/types"
16 15
 	"gotest.tools/v3/assert"
17 16
 	is "gotest.tools/v3/assert/cmp"
... ...
@@ -30,7 +29,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) {
30 30
 		{
31 31
 			doc:             "default api version",
32 32
 			envs:            map[string]string{},
33
-			expectedVersion: api.DefaultVersion,
33
+			expectedVersion: DefaultAPIVersion,
34 34
 		},
35 35
 		{
36 36
 			doc: "invalid cert path",
... ...
@@ -44,7 +43,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) {
44 44
 			envs: map[string]string{
45 45
 				"DOCKER_CERT_PATH": "testdata/",
46 46
 			},
47
-			expectedVersion: api.DefaultVersion,
47
+			expectedVersion: DefaultAPIVersion,
48 48
 		},
49 49
 		{
50 50
 			doc: "default api version with cert path and tls verify",
... ...
@@ -52,7 +51,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) {
52 52
 				"DOCKER_CERT_PATH":  "testdata/",
53 53
 				"DOCKER_TLS_VERIFY": "1",
54 54
 			},
55
-			expectedVersion: api.DefaultVersion,
55
+			expectedVersion: DefaultAPIVersion,
56 56
 		},
57 57
 		{
58 58
 			doc: "default api version with cert path and host",
... ...
@@ -60,7 +59,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) {
60 60
 				"DOCKER_CERT_PATH": "testdata/",
61 61
 				"DOCKER_HOST":      "https://notaunixsocket",
62 62
 			},
63
-			expectedVersion: api.DefaultVersion,
63
+			expectedVersion: DefaultAPIVersion,
64 64
 		},
65 65
 		{
66 66
 			doc: "invalid docker host",
... ...
@@ -74,7 +73,7 @@ func TestNewClientWithOpsFromEnv(t *testing.T) {
74 74
 			envs: map[string]string{
75 75
 				"DOCKER_HOST": "invalid://url",
76 76
 			},
77
-			expectedVersion: api.DefaultVersion,
77
+			expectedVersion: DefaultAPIVersion,
78 78
 		},
79 79
 		{
80 80
 			doc: "override api version",
... ...
@@ -117,17 +116,17 @@ func TestGetAPIPath(t *testing.T) {
117 117
 	}{
118 118
 		{
119 119
 			path:     "/containers/json",
120
-			expected: "/v" + api.DefaultVersion + "/containers/json",
120
+			expected: "/v" + DefaultAPIVersion + "/containers/json",
121 121
 		},
122 122
 		{
123 123
 			path:     "/containers/json",
124 124
 			query:    url.Values{},
125
-			expected: "/v" + api.DefaultVersion + "/containers/json",
125
+			expected: "/v" + DefaultAPIVersion + "/containers/json",
126 126
 		},
127 127
 		{
128 128
 			path:     "/containers/json",
129 129
 			query:    url.Values{"s": []string{"c"}},
130
-			expected: "/v" + api.DefaultVersion + "/containers/json?s=c",
130
+			expected: "/v" + DefaultAPIVersion + "/containers/json?s=c",
131 131
 		},
132 132
 		{
133 133
 			version:  "1.22",
... ...
@@ -235,7 +234,7 @@ func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) {
235 235
 
236 236
 	client, err := NewClientWithOpts(FromEnv)
237 237
 	assert.NilError(t, err)
238
-	assert.Check(t, is.Equal(client.ClientVersion(), api.DefaultVersion))
238
+	assert.Check(t, is.Equal(client.ClientVersion(), DefaultAPIVersion))
239 239
 
240 240
 	const expected = "1.22"
241 241
 	t.Setenv("DOCKER_API_VERSION", expected)
... ...
@@ -375,8 +374,8 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) {
375 375
 	)
376 376
 	assert.NilError(t, err)
377 377
 
378
-	// Client defaults to use api.DefaultVersion before version-negotiation.
379
-	expected := api.DefaultVersion
378
+	// Client defaults to use DefaultAPIVersion before version-negotiation.
379
+	expected := DefaultAPIVersion
380 380
 	assert.Check(t, is.Equal(client.ClientVersion(), expected))
381 381
 
382 382
 	// First request should trigger negotiation
... ...
@@ -423,7 +422,7 @@ func TestCustomAPIVersion(t *testing.T) {
423 423
 	}{
424 424
 		{
425 425
 			version:  "",
426
-			expected: api.DefaultVersion,
426
+			expected: DefaultAPIVersion,
427 427
 		},
428 428
 		{
429 429
 			version:  "1.0",
... ...
@@ -435,7 +434,7 @@ func TestCustomAPIVersion(t *testing.T) {
435 435
 		},
436 436
 		{
437 437
 			version:  "v",
438
-			expected: api.DefaultVersion,
438
+			expected: DefaultAPIVersion,
439 439
 		},
440 440
 		{
441 441
 			version:  "v1.0",
... ...
@@ -194,6 +194,10 @@ func WithTLSClientConfigFromEnv() Opt {
194 194
 // WithVersion overrides the client version with the specified one. If an empty
195 195
 // version is provided, the value is ignored to allow version negotiation
196 196
 // (see [WithAPIVersionNegotiation]).
197
+//
198
+// WithVersion does not validate if the client supports the given version,
199
+// and callers should verify if the version is in the correct format and
200
+// lower than the maximum supported version as defined by [DefaultAPIVersion].
197 201
 func WithVersion(version string) Opt {
198 202
 	return func(c *Client) error {
199 203
 		if v := strings.TrimPrefix(version, "v"); v != "" {
... ...
@@ -208,6 +212,10 @@ func WithVersion(version string) Opt {
208 208
 // the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
209 209
 // If DOCKER_API_VERSION is not set, or set to an empty value, the version
210 210
 // is not modified.
211
+//
212
+// WithVersion does not validate if the client supports the given version,
213
+// and callers should verify if the version is in the correct format and
214
+// lower than the maximum supported version as defined by [DefaultAPIVersion].
211 215
 func WithVersionFromEnv() Opt {
212 216
 	return func(c *Client) error {
213 217
 		return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c)
... ...
@@ -7,7 +7,6 @@ import (
7 7
 	"testing"
8 8
 	"time"
9 9
 
10
-	"github.com/docker/docker/api"
11 10
 	"gotest.tools/v3/assert"
12 11
 	is "gotest.tools/v3/assert/cmp"
13 12
 )
... ...
@@ -50,7 +49,7 @@ func TestOptionWithVersionFromEnv(t *testing.T) {
50 50
 	c, err := NewClientWithOpts(WithVersionFromEnv())
51 51
 	assert.NilError(t, err)
52 52
 	assert.Check(t, c.client != nil)
53
-	assert.Check(t, is.Equal(c.version, api.DefaultVersion))
53
+	assert.Check(t, is.Equal(c.version, DefaultAPIVersion))
54 54
 	assert.Check(t, is.Equal(c.manualOverride, false))
55 55
 
56 56
 	t.Setenv("DOCKER_API_VERSION", "2.9999")