client: enable API-version negotiation by default

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-11-12 16:39:20 +01:00
parent e752ec0f8e
commit 189942570a
23 changed files with 58 additions and 61 deletions

View File

@@ -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)
}

View File

@@ -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{

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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"}})

View File

@@ -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{})

View File

@@ -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)
}

View File

@@ -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{})

View File

@@ -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{})

View File

@@ -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{})

View File

@@ -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"}})

View File

@@ -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{})

View File

@@ -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{})

View File

@@ -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)
}

View File

@@ -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{})

View File

@@ -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)
}

View File

@@ -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{})

View File

@@ -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{})

View File

@@ -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)
}

View File

@@ -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{

View File

@@ -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
}
}

View File

@@ -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)
}