Browse code

client: remove support for negotiating API version < v1.44 (docker 25.0)

Docker versions below 25.0 have reached EOL; 25.0 is currently maintained
as an LTS version by Mirantis, and we want to allow current versions of the
CLI to be able to connect to such setups.

This patch raises the fallback API version to API v1.44; when negotiating an API
version with a daemon, this will be the lowest version negotiated.

Currently, it still allows manually overriding the version to versions that
are not supported (`WithVersion`, `WithVersionFromEnv`), and no code has
been removed yet that adjusts the client for old API versions, but this
can be done in a follow-up.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/10/07 01:38:38
Showing 3 changed files
... ...
@@ -54,6 +54,7 @@ import (
54 54
 	"sync/atomic"
55 55
 	"time"
56 56
 
57
+	cerrdefs "github.com/containerd/errdefs"
57 58
 	"github.com/docker/go-connections/sockets"
58 59
 	"github.com/moby/moby/api/types"
59 60
 	"github.com/moby/moby/api/types/versions"
... ...
@@ -102,7 +103,7 @@ const MaxAPIVersion = "1.52"
102 102
 // fallbackAPIVersion is the version to fall back to if API-version negotiation
103 103
 // fails. API versions below this version are not supported by the client,
104 104
 // and not considered when negotiating.
105
-const fallbackAPIVersion = "1.24"
105
+const fallbackAPIVersion = "1.44"
106 106
 
107 107
 // Ensure that Client always implements APIClient.
108 108
 var _ APIClient = &Client{}
... ...
@@ -273,7 +274,7 @@ func (cli *Client) checkVersion(ctx context.Context) error {
273 273
 		if err != nil {
274 274
 			return err
275 275
 		}
276
-		cli.negotiateAPIVersionPing(ping.APIVersion)
276
+		return cli.negotiateAPIVersion(ping.APIVersion)
277 277
 	}
278 278
 	return nil
279 279
 }
... ...
@@ -321,7 +322,8 @@ func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
321 321
 			// FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it.
322 322
 			return
323 323
 		}
324
-		cli.negotiateAPIVersionPing(ping.APIVersion)
324
+		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
325
+		_ = cli.negotiateAPIVersion(ping.APIVersion)
325 326
 	}
326 327
 }
327 328
 
... ...
@@ -342,16 +344,22 @@ func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) {
342 342
 		cli.negotiateLock.Lock()
343 343
 		defer cli.negotiateLock.Unlock()
344 344
 
345
-		cli.negotiateAPIVersionPing(pingResponse.APIVersion)
345
+		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
346
+		_ = cli.negotiateAPIVersion(pingResponse.APIVersion)
346 347
 	}
347 348
 }
348 349
 
349
-// negotiateAPIVersionPing queries the API and updates the version to match the
350
-// API version from the ping response.
351
-func (cli *Client) negotiateAPIVersionPing(pingVersion string) {
350
+// negotiateAPIVersion updates the version to match the API version from
351
+// the ping response. It falls back to the lowest version supported if the
352
+// API version is empty, or returns an error if the API version is lower than
353
+// the lowest supported API version, in which case the version is not modified.
354
+func (cli *Client) negotiateAPIVersion(pingVersion string) error {
352 355
 	pingVersion = strings.TrimPrefix(pingVersion, "v")
353 356
 	if pingVersion == "" {
357
+		// TODO(thaJeztah): consider returning an error on empty value or not falling back; see https://github.com/moby/moby/pull/51119#discussion_r2413148487
354 358
 		pingVersion = fallbackAPIVersion
359
+	} else if versions.LessThan(pingVersion, fallbackAPIVersion) {
360
+		return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, fallbackAPIVersion))
355 361
 	}
356 362
 
357 363
 	// if the client is not initialized with a version, start with the latest supported version
... ...
@@ -369,6 +377,7 @@ func (cli *Client) negotiateAPIVersionPing(pingVersion string) {
369 369
 	if cli.negotiateVersion {
370 370
 		cli.negotiated.Store(true)
371 371
 	}
372
+	return nil
372 373
 }
373 374
 
374 375
 // DaemonHost returns the host address used by the client
... ...
@@ -82,6 +82,13 @@ func TestNewClientWithOpsFromEnv(t *testing.T) {
82 82
 			},
83 83
 			expectedVersion: "1.50",
84 84
 		},
85
+		{
86
+			doc: "override with unsupported api version",
87
+			envs: map[string]string{
88
+				"DOCKER_API_VERSION": "1.0",
89
+			},
90
+			expectedVersion: "1.0",
91
+		},
85 92
 	}
86 93
 
87 94
 	for _, tc := range testcases {
... ...
@@ -296,13 +303,11 @@ func TestNegotiateAPIVersion(t *testing.T) {
296 296
 			expectedVersion: fallbackAPIVersion,
297 297
 		},
298 298
 		{
299
-			// client should downgrade to the version reported by the daemon.
300
-			// version negotiation was added in API 1.25, so this is theoretical,
301
-			// but it should negotiate to versions before that if the daemon
302
-			// gives that as a response.
303
-			doc:             "downgrade old",
299
+			// client should not downgrade to the version reported by the daemon
300
+			// if the version is not supported.
301
+			doc:             "no downgrade old",
304 302
 			pingVersion:     "1.19",
305
-			expectedVersion: "1.19",
303
+			expectedVersion: MaxAPIVersion,
306 304
 		},
307 305
 		{
308 306
 			// client should not upgrade to a newer version if a version was set,
... ...
@@ -54,6 +54,7 @@ import (
54 54
 	"sync/atomic"
55 55
 	"time"
56 56
 
57
+	cerrdefs "github.com/containerd/errdefs"
57 58
 	"github.com/docker/go-connections/sockets"
58 59
 	"github.com/moby/moby/api/types"
59 60
 	"github.com/moby/moby/api/types/versions"
... ...
@@ -102,7 +103,7 @@ const MaxAPIVersion = "1.52"
102 102
 // fallbackAPIVersion is the version to fall back to if API-version negotiation
103 103
 // fails. API versions below this version are not supported by the client,
104 104
 // and not considered when negotiating.
105
-const fallbackAPIVersion = "1.24"
105
+const fallbackAPIVersion = "1.44"
106 106
 
107 107
 // Ensure that Client always implements APIClient.
108 108
 var _ APIClient = &Client{}
... ...
@@ -273,7 +274,7 @@ func (cli *Client) checkVersion(ctx context.Context) error {
273 273
 		if err != nil {
274 274
 			return err
275 275
 		}
276
-		cli.negotiateAPIVersionPing(ping.APIVersion)
276
+		return cli.negotiateAPIVersion(ping.APIVersion)
277 277
 	}
278 278
 	return nil
279 279
 }
... ...
@@ -321,7 +322,8 @@ func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
321 321
 			// FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it.
322 322
 			return
323 323
 		}
324
-		cli.negotiateAPIVersionPing(ping.APIVersion)
324
+		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
325
+		_ = cli.negotiateAPIVersion(ping.APIVersion)
325 326
 	}
326 327
 }
327 328
 
... ...
@@ -342,16 +344,22 @@ func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) {
342 342
 		cli.negotiateLock.Lock()
343 343
 		defer cli.negotiateLock.Unlock()
344 344
 
345
-		cli.negotiateAPIVersionPing(pingResponse.APIVersion)
345
+		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
346
+		_ = cli.negotiateAPIVersion(pingResponse.APIVersion)
346 347
 	}
347 348
 }
348 349
 
349
-// negotiateAPIVersionPing queries the API and updates the version to match the
350
-// API version from the ping response.
351
-func (cli *Client) negotiateAPIVersionPing(pingVersion string) {
350
+// negotiateAPIVersion updates the version to match the API version from
351
+// the ping response. It falls back to the lowest version supported if the
352
+// API version is empty, or returns an error if the API version is lower than
353
+// the lowest supported API version, in which case the version is not modified.
354
+func (cli *Client) negotiateAPIVersion(pingVersion string) error {
352 355
 	pingVersion = strings.TrimPrefix(pingVersion, "v")
353 356
 	if pingVersion == "" {
357
+		// TODO(thaJeztah): consider returning an error on empty value or not falling back; see https://github.com/moby/moby/pull/51119#discussion_r2413148487
354 358
 		pingVersion = fallbackAPIVersion
359
+	} else if versions.LessThan(pingVersion, fallbackAPIVersion) {
360
+		return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, fallbackAPIVersion))
355 361
 	}
356 362
 
357 363
 	// if the client is not initialized with a version, start with the latest supported version
... ...
@@ -369,6 +377,7 @@ func (cli *Client) negotiateAPIVersionPing(pingVersion string) {
369 369
 	if cli.negotiateVersion {
370 370
 		cli.negotiated.Store(true)
371 371
 	}
372
+	return nil
372 373
 }
373 374
 
374 375
 // DaemonHost returns the host address used by the client