From 189942570a2399de4e3af3a821339fd8db9229fc Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 12 Nov 2025 16:39:20 +0100 Subject: [PATCH] client: enable API-version negotiation by default Signed-off-by: Sebastiaan van Stijn --- client/README.md | 2 +- client/client.go | 25 +++++++++++-------- client/client_example_test.go | 2 +- client/client_options.go | 10 +++----- client/client_test.go | 3 --- client/container_create_test.go | 2 +- client/container_exec_test.go | 2 +- client/container_logs_test.go | 2 +- client/container_restart_test.go | 2 +- client/container_stop_test.go | 2 +- client/container_wait_test.go | 2 +- client/hijack_test.go | 2 +- client/image_list_test.go | 2 +- client/network_create_test.go | 2 +- client/ping.go | 7 +++--- client/service_create_test.go | 2 +- client/service_logs_test.go | 2 +- client/service_update_test.go | 2 +- client/volume_remove_test.go | 2 +- vendor/github.com/moby/moby/client/README.md | 2 +- vendor/github.com/moby/moby/client/client.go | 25 +++++++++++-------- .../moby/moby/client/client_options.go | 10 +++----- vendor/github.com/moby/moby/client/ping.go | 7 +++--- 23 files changed, 58 insertions(+), 61 deletions(-) diff --git a/client/README.md b/client/README.md index 803567a2e2..115e604dbc 100644 --- a/client/README.md +++ b/client/README.md @@ -27,7 +27,7 @@ func main() { // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does // API-version negotiation to allow downgrading the API version // when connecting with an older daemon version. - apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) + apiClient, err := client.New(client.FromEnv) if err != nil { panic(err) } diff --git a/client/client.go b/client/client.go index 07df7a3d4a..abe85457f3 100644 --- a/client/client.go +++ b/client/client.go @@ -8,10 +8,8 @@ https://docs.docker.com/reference/api/engine/ You use the library by constructing a client object using [New] and calling methods on it. The client can be configured from environment -variables by passing the [FromEnv] option, and the [WithAPIVersionNegotiation] -option to allow downgrading the API version used when connecting with an older -daemon version. Other options can be configured manually by passing any of -the available [Opt] options. +variables by passing the [FromEnv] option. Other options can be configured +manually by passing any of the available [Opt] options. For example, to list running containers (the equivalent of "docker ps"): @@ -30,7 +28,7 @@ For example, to list running containers (the equivalent of "docker ps"): // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does // API-version negotiation to allow downgrading the API version // when connecting with an older daemon version. - apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) + apiClient, err := client.New(client.FromEnv) if err != nil { log.Fatal(err) } @@ -103,9 +101,9 @@ import ( const DummyHost = "api.moby.localhost" // MaxAPIVersion is the highest REST API version supported by the client. -// If API-version negotiation is enabled (see [WithAPIVersionNegotiation], -// the client may downgrade its API version. Similarly, the [WithAPIVersion] -// and [WithAPIVersionFromEnv] options allow overriding the version. +// If API-version negotiation is enabled, the client may downgrade its API version. +// Similarly, the [WithAPIVersion] and [WithAPIVersionFromEnv] options allow +// overriding the version and disable API-version negotiation. // // This version may be lower than the version of the api library module used. const MaxAPIVersion = "1.52" @@ -172,8 +170,13 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) { // It takes an optional list of [Opt] functional arguments, which are applied in // the order they're provided, which allows modifying the defaults when creating // the client. For example, the following initializes a client that configures -// itself with values from environment variables ([FromEnv]), and has automatic -// API version negotiation enabled ([WithAPIVersionNegotiation]). +// itself with values from environment variables ([FromEnv]). +// +// By default, the client automatically negotiates the API version to use when +// making requests. API version negotiation is performed on the first request; +// subsequent requests do not re-negotiate. Use [WithAPIVersion] or +// [WithAPIVersionFromEnv] to configure the client with a fixed API version +// and disable API version negotiation. // // cli, err := client.New( // client.FromEnv, @@ -282,7 +285,7 @@ func (cli *Client) Close() error { // be negotiated when making the actual requests, and for which cases // we cannot do the negotiation lazily. func (cli *Client) checkVersion(ctx context.Context) error { - if cli.negotiated.Load() || !cli.negotiateVersion { + if cli.negotiated.Load() { return nil } _, err := cli.Ping(ctx, PingOptions{ diff --git a/client/client_example_test.go b/client/client_example_test.go index b453ee860e..2edb8bac86 100644 --- a/client/client_example_test.go +++ b/client/client_example_test.go @@ -13,7 +13,7 @@ func ExampleNew() { // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does // API-version negotiation to allow downgrading the API version // when connecting with an older daemon version. - apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) + apiClient, err := client.New(client.FromEnv) if err != nil { log.Fatal(err) } diff --git a/client/client_options.go b/client/client_options.go index 7236f88638..295d299180 100644 --- a/client/client_options.go +++ b/client/client_options.go @@ -55,12 +55,6 @@ type clientConfig struct { // takes precedence. Either field disables API-version negotiation. envAPIVersion string - // 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 - // traceOpts is a list of options to configure the tracing span. traceOpts []otelhttp.Option } @@ -325,9 +319,11 @@ func WithVersionFromEnv() Opt { // 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 do not re-negotiate. +// +// Deprecated: API-version negotiation is now enabled by default. Use [WithAPIVersion] +// or [WithAPIVersionFromEnv] to disable API version negotiation. func WithAPIVersionNegotiation() Opt { return func(c *clientConfig) error { - c.negotiateVersion = true return nil } } diff --git a/client/client_test.go b/client/client_test.go index 248616fa34..b7aad6fca9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -257,7 +257,6 @@ func TestNegotiateAPIVersionEmpty(t *testing.T) { const expected = MinAPIVersion client, err := New(FromEnv, - WithAPIVersionNegotiation(), WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: expected})), ) assert.NilError(t, err) @@ -330,7 +329,6 @@ func TestNegotiateAPIVersion(t *testing.T) { t.Run(tc.doc, func(t *testing.T) { opts := []Opt{ FromEnv, - WithAPIVersionNegotiation(), WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: tc.pingVersion})), } @@ -396,7 +394,6 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) { WithBaseMockClient(func(req *http.Request) (*http.Response, error) { return mockPingResponse(http.StatusOK, PingResult{APIVersion: pingVersion})(req) }), - WithAPIVersionNegotiation(), ) assert.NilError(t, err) diff --git a/client/container_create_test.go b/client/container_create_test.go index 03a7e4b5e3..7a18d33a4d 100644 --- a/client/container_create_test.go +++ b/client/container_create_test.go @@ -87,7 +87,7 @@ func TestContainerCreateAutoRemove(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestContainerCreateConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.ContainerCreate(t.Context(), ContainerCreateOptions{Config: &container.Config{Image: "test"}}) diff --git a/client/container_exec_test.go b/client/container_exec_test.go index a68fc6aef4..27e0672a5c 100644 --- a/client/container_exec_test.go +++ b/client/container_exec_test.go @@ -36,7 +36,7 @@ func TestExecCreateError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestExecCreateConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.ExecCreate(t.Context(), "container_id", ExecCreateOptions{}) diff --git a/client/container_logs_test.go b/client/container_logs_test.go index 128bb8dae3..78bfdabe84 100644 --- a/client/container_logs_test.go +++ b/client/container_logs_test.go @@ -167,7 +167,7 @@ func TestContainerLogs(t *testing.T) { } func ExampleClient_ContainerLogs_withTimeout() { - client, err := New(FromEnv, WithAPIVersionNegotiation()) + client, err := New(FromEnv) if err != nil { log.Fatal(err) } diff --git a/client/container_restart_test.go b/client/container_restart_test.go index 9f1ff61402..026aec407b 100644 --- a/client/container_restart_test.go +++ b/client/container_restart_test.go @@ -30,7 +30,7 @@ func TestContainerRestartError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestContainerRestartConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.ContainerRestart(t.Context(), "nothing", ContainerRestartOptions{}) diff --git a/client/container_stop_test.go b/client/container_stop_test.go index 8522f43bdf..f953fa0aa5 100644 --- a/client/container_stop_test.go +++ b/client/container_stop_test.go @@ -30,7 +30,7 @@ func TestContainerStopError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestContainerStopConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.ContainerStop(t.Context(), "container_id", ContainerStopOptions{}) diff --git a/client/container_wait_test.go b/client/container_wait_test.go index 6af24b4e3b..568991b9d8 100644 --- a/client/container_wait_test.go +++ b/client/container_wait_test.go @@ -41,7 +41,7 @@ func TestContainerWaitConnectionError(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) wait := client.ContainerWait(ctx, "nothing", ContainerWaitOptions{}) diff --git a/client/hijack_test.go b/client/hijack_test.go index 35e3d4f6cb..0c1c771c12 100644 --- a/client/hijack_test.go +++ b/client/hijack_test.go @@ -105,7 +105,7 @@ func TestTLSCloseWriter(t *testing.T) { httpClient := ts.Client() defer httpClient.CloseIdleConnections() - client, err := New(WithHost("tcp://"+serverURL.Host), WithHTTPClient(httpClient), WithAPIVersionNegotiation()) + client, err := New(WithHost("tcp://"+serverURL.Host), WithHTTPClient(httpClient)) assert.NilError(t, err) resp, err := client.postHijacked(ctx, "/asdf", url.Values{}, nil, map[string][]string{"Content-Type": {"text/plain"}}) diff --git a/client/image_list_test.go b/client/image_list_test.go index aa2225a02c..f81664dc28 100644 --- a/client/image_list_test.go +++ b/client/image_list_test.go @@ -24,7 +24,7 @@ func TestImageListError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestImageListConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.ImageList(t.Context(), ImageListOptions{}) diff --git a/client/network_create_test.go b/client/network_create_test.go index 3ab37e30ba..5220d406b4 100644 --- a/client/network_create_test.go +++ b/client/network_create_test.go @@ -23,7 +23,7 @@ func TestNetworkCreateError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestNetworkCreateConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.NetworkCreate(t.Context(), "mynetwork", NetworkCreateOptions{}) diff --git a/client/ping.go b/client/ping.go index a95a249868..5d4b85fe59 100644 --- a/client/ping.go +++ b/client/ping.go @@ -71,11 +71,12 @@ type SwarmStatus struct { // for other non-success status codes, failing to connect to the API, or failing // to parse the API response. func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, error) { - if cli.negotiated.Load() && !options.ForceNegotiate { - // API version was already negotiated or manually set. + if !options.NegotiateAPIVersion { + // No API version negotiation needed; just return ping response. return cli.ping(ctx) } - if !options.NegotiateAPIVersion && !cli.negotiateVersion { + if cli.negotiated.Load() && !options.ForceNegotiate { + // API version was already negotiated or manually set. return cli.ping(ctx) } diff --git a/client/service_create_test.go b/client/service_create_test.go index 04f946e8b0..afc6edf6b6 100644 --- a/client/service_create_test.go +++ b/client/service_create_test.go @@ -29,7 +29,7 @@ func TestServiceCreateError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestServiceCreateConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.ServiceCreate(t.Context(), ServiceCreateOptions{}) diff --git a/client/service_logs_test.go b/client/service_logs_test.go index 9b063a2503..efaebb3d62 100644 --- a/client/service_logs_test.go +++ b/client/service_logs_test.go @@ -129,7 +129,7 @@ func TestServiceLogs(t *testing.T) { } func ExampleClient_ServiceLogs_withTimeout() { - client, err := New(FromEnv, WithAPIVersionNegotiation()) + client, err := New(FromEnv) if err != nil { log.Fatal(err) } diff --git a/client/service_update_test.go b/client/service_update_test.go index b547ebb524..55d1c4c36a 100644 --- a/client/service_update_test.go +++ b/client/service_update_test.go @@ -32,7 +32,7 @@ func TestServiceUpdateError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestServiceUpdateConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.ServiceUpdate(t.Context(), "service_id", ServiceUpdateOptions{}) diff --git a/client/volume_remove_test.go b/client/volume_remove_test.go index e50f8c077b..94b9797559 100644 --- a/client/volume_remove_test.go +++ b/client/volume_remove_test.go @@ -31,7 +31,7 @@ func TestVolumeRemoveError(t *testing.T) { // // Regression test for https://github.com/docker/cli/issues/4890 func TestVolumeRemoveConnectionError(t *testing.T) { - client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid")) + client, err := New(WithHost("tcp://no-such-host.invalid")) assert.NilError(t, err) _, err = client.VolumeRemove(t.Context(), "volume_id", VolumeRemoveOptions{}) diff --git a/vendor/github.com/moby/moby/client/README.md b/vendor/github.com/moby/moby/client/README.md index 803567a2e2..115e604dbc 100644 --- a/vendor/github.com/moby/moby/client/README.md +++ b/vendor/github.com/moby/moby/client/README.md @@ -27,7 +27,7 @@ func main() { // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does // API-version negotiation to allow downgrading the API version // when connecting with an older daemon version. - apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) + apiClient, err := client.New(client.FromEnv) if err != nil { panic(err) } diff --git a/vendor/github.com/moby/moby/client/client.go b/vendor/github.com/moby/moby/client/client.go index 07df7a3d4a..abe85457f3 100644 --- a/vendor/github.com/moby/moby/client/client.go +++ b/vendor/github.com/moby/moby/client/client.go @@ -8,10 +8,8 @@ https://docs.docker.com/reference/api/engine/ You use the library by constructing a client object using [New] and calling methods on it. The client can be configured from environment -variables by passing the [FromEnv] option, and the [WithAPIVersionNegotiation] -option to allow downgrading the API version used when connecting with an older -daemon version. Other options can be configured manually by passing any of -the available [Opt] options. +variables by passing the [FromEnv] option. Other options can be configured +manually by passing any of the available [Opt] options. For example, to list running containers (the equivalent of "docker ps"): @@ -30,7 +28,7 @@ For example, to list running containers (the equivalent of "docker ps"): // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does // API-version negotiation to allow downgrading the API version // when connecting with an older daemon version. - apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) + apiClient, err := client.New(client.FromEnv) if err != nil { log.Fatal(err) } @@ -103,9 +101,9 @@ import ( const DummyHost = "api.moby.localhost" // MaxAPIVersion is the highest REST API version supported by the client. -// If API-version negotiation is enabled (see [WithAPIVersionNegotiation], -// the client may downgrade its API version. Similarly, the [WithAPIVersion] -// and [WithAPIVersionFromEnv] options allow overriding the version. +// If API-version negotiation is enabled, the client may downgrade its API version. +// Similarly, the [WithAPIVersion] and [WithAPIVersionFromEnv] options allow +// overriding the version and disable API-version negotiation. // // This version may be lower than the version of the api library module used. const MaxAPIVersion = "1.52" @@ -172,8 +170,13 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) { // It takes an optional list of [Opt] functional arguments, which are applied in // the order they're provided, which allows modifying the defaults when creating // the client. For example, the following initializes a client that configures -// itself with values from environment variables ([FromEnv]), and has automatic -// API version negotiation enabled ([WithAPIVersionNegotiation]). +// itself with values from environment variables ([FromEnv]). +// +// By default, the client automatically negotiates the API version to use when +// making requests. API version negotiation is performed on the first request; +// subsequent requests do not re-negotiate. Use [WithAPIVersion] or +// [WithAPIVersionFromEnv] to configure the client with a fixed API version +// and disable API version negotiation. // // cli, err := client.New( // client.FromEnv, @@ -282,7 +285,7 @@ func (cli *Client) Close() error { // be negotiated when making the actual requests, and for which cases // we cannot do the negotiation lazily. func (cli *Client) checkVersion(ctx context.Context) error { - if cli.negotiated.Load() || !cli.negotiateVersion { + if cli.negotiated.Load() { return nil } _, err := cli.Ping(ctx, PingOptions{ diff --git a/vendor/github.com/moby/moby/client/client_options.go b/vendor/github.com/moby/moby/client/client_options.go index 7236f88638..295d299180 100644 --- a/vendor/github.com/moby/moby/client/client_options.go +++ b/vendor/github.com/moby/moby/client/client_options.go @@ -55,12 +55,6 @@ type clientConfig struct { // takes precedence. Either field disables API-version negotiation. envAPIVersion string - // 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 - // traceOpts is a list of options to configure the tracing span. traceOpts []otelhttp.Option } @@ -325,9 +319,11 @@ func WithVersionFromEnv() Opt { // 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 do not re-negotiate. +// +// Deprecated: API-version negotiation is now enabled by default. Use [WithAPIVersion] +// or [WithAPIVersionFromEnv] to disable API version negotiation. func WithAPIVersionNegotiation() Opt { return func(c *clientConfig) error { - c.negotiateVersion = true return nil } } diff --git a/vendor/github.com/moby/moby/client/ping.go b/vendor/github.com/moby/moby/client/ping.go index a95a249868..5d4b85fe59 100644 --- a/vendor/github.com/moby/moby/client/ping.go +++ b/vendor/github.com/moby/moby/client/ping.go @@ -71,11 +71,12 @@ type SwarmStatus struct { // for other non-success status codes, failing to connect to the API, or failing // to parse the API response. func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, error) { - if cli.negotiated.Load() && !options.ForceNegotiate { - // API version was already negotiated or manually set. + if !options.NegotiateAPIVersion { + // No API version negotiation needed; just return ping response. return cli.ping(ctx) } - if !options.NegotiateAPIVersion && !cli.negotiateVersion { + if cli.negotiated.Load() && !options.ForceNegotiate { + // API version was already negotiated or manually set. return cli.ping(ctx) }