From d9c4582c8f80f79fe45eca22b127be81f09ded66 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Mon, 30 Mar 2026 17:19:43 +0200 Subject: [PATCH 1/4] pkg/compose: use negotiated API version for request shaping Move runtimeVersionCache from a package-level var to per-instance fields on composeService and add CurrentAPIVersion() that negotiates via Ping before returning the client version. Switch getCreateConfigs and buildContainerVolumes to use CurrentAPIVersion so that version-gated request shaping matches what the daemon actually validates against (the negotiated API version from the request path, not the server's max capability). Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Guillaume Lours --- pkg/compose/compose.go | 44 +++++++++++++++++++++++++++------ pkg/compose/convergence_test.go | 29 +++++++++++++++++++--- pkg/compose/create.go | 6 +++-- pkg/compose/images.go | 4 ++- pkg/compose/images_test.go | 3 ++- 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 4eddfe4677b..03896102cf7 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -215,6 +215,9 @@ type composeService struct { clock clockwork.Clock maxConcurrency int dryRun bool + + runtimeVersion runtimeVersionCache + currentAPIVersion runtimeVersionCache } // Close releases any connections/resources held by the underlying clients. @@ -497,16 +500,43 @@ type runtimeVersionCache struct { err error } -var runtimeVersion runtimeVersionCache - +// RuntimeVersion returns the raw API version reported by the daemon. +// Callers that need the negotiated/effective client API version should use +// CurrentAPIVersion instead. func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) { - // TODO(thaJeztah): this should use Client.ClientVersion), which has the negotiated version. - runtimeVersion.once.Do(func() { + s.runtimeVersion.once.Do(func() { version, err := s.apiClient().ServerVersion(ctx, client.ServerVersionOptions{}) if err != nil { - runtimeVersion.err = err + s.runtimeVersion.err = err + return } - runtimeVersion.val = version.APIVersion + s.runtimeVersion.val = version.APIVersion }) - return runtimeVersion.val, runtimeVersion.err + return s.runtimeVersion.val, s.runtimeVersion.err +} + +// CurrentAPIVersion returns the API version currently used by the Docker client. +// Trigger negotiation first so version-gated request shaping matches the version +// that subsequent API calls will actually use. +func (s *composeService) CurrentAPIVersion(ctx context.Context) (string, error) { + s.currentAPIVersion.once.Do(func() { + cli := s.apiClient() + _, err := cli.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true}) + if err != nil { + s.currentAPIVersion.err = err + return + } + + version := cli.ClientVersion() + if version != "" { + s.currentAPIVersion.val = version + return + } + + // Defensive fallback for unexpected client implementations or mocks that + // do not populate ClientVersion after a successful negotiated ping. + s.currentAPIVersion.val, s.currentAPIVersion.err = s.RuntimeVersion(ctx) + }) + + return s.currentAPIVersion.val, s.currentAPIVersion.err } diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go index 901f43ccea3..9524435d252 100644 --- a/pkg/compose/convergence_test.go +++ b/pkg/compose/convergence_test.go @@ -411,11 +411,10 @@ func TestCreateMobyContainer(t *testing.T) { apiClient.EXPECT().DaemonHost().Return("").AnyTimes() apiClient.EXPECT().ImageInspect(anyCancellableContext(), gomock.Any()).Return(client.ImageInspectResult{}, nil).AnyTimes() - // force `RuntimeVersion` to fetch fresh version - runtimeVersion = runtimeVersionCache{} - apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(client.ServerVersionResult{ + apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).Return(client.PingResult{ APIVersion: "1.44", }, nil).AnyTimes() + apiClient.EXPECT().ClientVersion().Return("1.44").AnyTimes() service := types.ServiceConfig{ Name: "test", @@ -498,3 +497,27 @@ func TestCreateMobyContainer(t *testing.T) { assert.DeepEqual(t, want, got, cmpopts.EquateComparable(netip.Addr{}), cmpopts.EquateEmpty()) assert.NilError(t, err) } + +func TestCurrentAPIVersionCachesNegotiation(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + apiClient := mocks.NewMockAPIClient(mockCtrl) + cli := mocks.NewMockCli(mockCtrl) + tested := &composeService{dockerCli: cli} + + cli.EXPECT().Client().Return(apiClient).AnyTimes() + + apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).Return(client.PingResult{ + APIVersion: "1.44", + }, nil).Times(1) + apiClient.EXPECT().ClientVersion().Return("1.43").Times(1) + + version, err := tested.CurrentAPIVersion(t.Context()) + assert.NilError(t, err) + assert.Equal(t, version, "1.43") + + version, err = tested.CurrentAPIVersion(t.Context()) + assert.NilError(t, err) + assert.Equal(t, version, "1.43") +} diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 23cba9f5d19..ce7eced9948 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -252,7 +252,7 @@ func (s *composeService) getCreateConfigs(ctx context.Context, if err != nil { return createConfigs{}, err } - apiVersion, err := s.RuntimeVersion(ctx) + apiVersion, err := s.CurrentAPIVersion(ctx) if err != nil { return createConfigs{}, err } @@ -897,7 +897,9 @@ func (s *composeService) buildContainerVolumes( } } case mount.TypeImage: - version, err := s.RuntimeVersion(ctx) + // The daemon validates image mounts against the negotiated API version + // from the request path, not the server's own max version. + version, err := s.CurrentAPIVersion(ctx) if err != nil { return nil, nil, err } diff --git a/pkg/compose/images.go b/pkg/compose/images.go index 155405213cc..24d37c77f28 100644 --- a/pkg/compose/images.go +++ b/pkg/compose/images.go @@ -56,7 +56,9 @@ func (s *composeService) Images(ctx context.Context, projectName string, options containers = allContainers.Items } - version, err := s.RuntimeVersion(ctx) + // The daemon validates the platform field in ImageInspect against the + // negotiated API version from the request path, not the server's own max version. + version, err := s.CurrentAPIVersion(ctx) if err != nil { return nil, err } diff --git a/pkg/compose/images_test.go b/pkg/compose/images_test.go index 25be1ac952c..9a7d2c12e20 100644 --- a/pkg/compose/images_test.go +++ b/pkg/compose/images_test.go @@ -41,7 +41,8 @@ func TestImages(t *testing.T) { args := projectFilter(strings.ToLower(testProject)) listOpts := client.ContainerListOptions{All: true, Filters: args} - api.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(client.ServerVersionResult{APIVersion: "1.96"}, nil).AnyTimes() + api.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).Return(client.PingResult{APIVersion: "1.96"}, nil).AnyTimes() + api.EXPECT().ClientVersion().Return("1.96").AnyTimes() timeStr1 := "2025-06-06T06:06:06.000000000Z" created1, _ := time.Parse(time.RFC3339Nano, timeStr1) timeStr2 := "2025-03-03T03:03:03.000000000Z" From 1b77d6141b34c1f814b1e056902be7b8a2208ece Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Mon, 30 Mar 2026 17:19:55 +0200 Subject: [PATCH 2/4] fix: use pointer receivers for composeService methods with sync.Once fields Moving runtimeVersionCache from a package-level var to instance fields on composeService caused copylocks violations in methods using value receivers, since sync.Once contains sync.noCopy. Switch the 4 affected methods to pointer receivers. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Guillaume Lours --- pkg/compose/build_bake.go | 4 ++-- pkg/compose/hook.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 8e7151830e5..dc691cca9ff 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -568,7 +568,7 @@ func dockerFilePath(ctxName string, dockerfile string) string { return dockerfile } -func (s composeService) dryRunBake(cfg bakeConfig) map[string]string { +func (s *composeService) dryRunBake(cfg bakeConfig) map[string]string { bakeResponse := map[string]string{} for name, target := range cfg.Targets { dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name))) @@ -581,7 +581,7 @@ func (s composeService) dryRunBake(cfg bakeConfig) map[string]string { return bakeResponse } -func (s composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) { +func (s *composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) { s.events.On(api.Resource{ ID: name + " ==>", Status: api.Done, diff --git a/pkg/compose/hook.go b/pkg/compose/hook.go index 54e55652ccf..8acc24011f9 100644 --- a/pkg/compose/hook.go +++ b/pkg/compose/hook.go @@ -31,7 +31,7 @@ import ( "github.com/docker/compose/v5/pkg/utils" ) -func (s composeService) runHook(ctx context.Context, ctr container.Summary, service types.ServiceConfig, hook types.ServiceHook, listener api.ContainerEventListener) error { +func (s *composeService) runHook(ctx context.Context, ctr container.Summary, service types.ServiceConfig, hook types.ServiceHook, listener api.ContainerEventListener) error { wOut := utils.GetWriter(func(line string) { listener(api.ContainerEvent{ Type: api.HookEventLog, @@ -96,7 +96,7 @@ func (s composeService) runHook(ctx context.Context, ctr container.Summary, serv return nil } -func (s composeService) runWaitExec(ctx context.Context, execID string, service types.ServiceConfig, listener api.ContainerEventListener) error { +func (s *composeService) runWaitExec(ctx context.Context, execID string, service types.ServiceConfig, listener api.ContainerEventListener) error { _, err := s.apiClient().ExecStart(ctx, execID, client.ExecStartOptions{ Detach: listener == nil, TTY: service.Tty, From 7e564c7de26b6755df326b7abf7f2d15ae993eee Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Mon, 30 Mar 2026 17:20:04 +0200 Subject: [PATCH 3/4] fix: don't cache transient errors in version negotiation Replace sync.Once with sync.Mutex so that only successful version lookups are cached. Errors (context cancellation, network blips) are returned without caching, allowing subsequent calls to retry. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Guillaume Lours --- pkg/compose/compose.go | 71 +++++++++++++++++++-------------- pkg/compose/convergence_test.go | 65 ++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 29 deletions(-) diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 03896102cf7..e6fce9be3b8 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -494,49 +494,62 @@ func (s *composeService) isSwarmEnabled(ctx context.Context) (bool, error) { return swarmEnabled.val, swarmEnabled.err } +// runtimeVersionCache caches a version string after a successful lookup. +// Errors (including context cancellation) are not cached so that +// subsequent calls can retry with a fresh context. type runtimeVersionCache struct { - once sync.Once - val string - err error + mu sync.Mutex + val string } // RuntimeVersion returns the raw API version reported by the daemon. // Callers that need the negotiated/effective client API version should use // CurrentAPIVersion instead. func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) { - s.runtimeVersion.once.Do(func() { - version, err := s.apiClient().ServerVersion(ctx, client.ServerVersionOptions{}) - if err != nil { - s.runtimeVersion.err = err - return - } - s.runtimeVersion.val = version.APIVersion - }) - return s.runtimeVersion.val, s.runtimeVersion.err + s.runtimeVersion.mu.Lock() + defer s.runtimeVersion.mu.Unlock() + if s.runtimeVersion.val != "" { + return s.runtimeVersion.val, nil + } + version, err := s.apiClient().ServerVersion(ctx, client.ServerVersionOptions{}) + if err != nil { + return "", err + } + s.runtimeVersion.val = version.APIVersion + return s.runtimeVersion.val, nil } // CurrentAPIVersion returns the API version currently used by the Docker client. // Trigger negotiation first so version-gated request shaping matches the version // that subsequent API calls will actually use. +// +// Lock ordering: currentAPIVersion.mu must be acquired before runtimeVersion.mu +// (via the RuntimeVersion fallback). No code path should reverse this order. func (s *composeService) CurrentAPIVersion(ctx context.Context) (string, error) { - s.currentAPIVersion.once.Do(func() { - cli := s.apiClient() - _, err := cli.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true}) - if err != nil { - s.currentAPIVersion.err = err - return - } + s.currentAPIVersion.mu.Lock() + defer s.currentAPIVersion.mu.Unlock() + if s.currentAPIVersion.val != "" { + return s.currentAPIVersion.val, nil + } - version := cli.ClientVersion() - if version != "" { - s.currentAPIVersion.val = version - return - } + cli := s.apiClient() + _, err := cli.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true}) + if err != nil { + return "", err + } - // Defensive fallback for unexpected client implementations or mocks that - // do not populate ClientVersion after a successful negotiated ping. - s.currentAPIVersion.val, s.currentAPIVersion.err = s.RuntimeVersion(ctx) - }) + version := cli.ClientVersion() + if version != "" { + s.currentAPIVersion.val = version + return s.currentAPIVersion.val, nil + } - return s.currentAPIVersion.val, s.currentAPIVersion.err + // Defensive fallback for unexpected client implementations or mocks that + // do not populate ClientVersion after a successful negotiated ping. + val, err := s.RuntimeVersion(ctx) + if err != nil { + return "", err + } + s.currentAPIVersion.val = val + return s.currentAPIVersion.val, nil } diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go index 9524435d252..7012576eb3f 100644 --- a/pkg/compose/convergence_test.go +++ b/pkg/compose/convergence_test.go @@ -521,3 +521,68 @@ func TestCurrentAPIVersionCachesNegotiation(t *testing.T) { assert.NilError(t, err) assert.Equal(t, version, "1.43") } + +func TestRuntimeVersionRetriesOnTransientError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + apiClient := mocks.NewMockAPIClient(mockCtrl) + cli := mocks.NewMockCli(mockCtrl) + tested := &composeService{dockerCli: cli} + + cli.EXPECT().Client().Return(apiClient).AnyTimes() + + // First call: ServerVersion fails with a transient error + firstCall := apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()). + Return(client.ServerVersionResult{}, context.DeadlineExceeded).Times(1) + + // Second call: succeeds + apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()). + Return(client.ServerVersionResult{APIVersion: "1.48"}, nil).Times(1).After(firstCall) + + _, err := tested.RuntimeVersion(t.Context()) + assert.ErrorIs(t, err, context.DeadlineExceeded) + + version, err := tested.RuntimeVersion(t.Context()) + assert.NilError(t, err) + assert.Equal(t, version, "1.48") + + // Third call returns cached value + version, err = tested.RuntimeVersion(t.Context()) + assert.NilError(t, err) + assert.Equal(t, version, "1.48") +} + +func TestCurrentAPIVersionRetriesOnTransientError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + apiClient := mocks.NewMockAPIClient(mockCtrl) + cli := mocks.NewMockCli(mockCtrl) + tested := &composeService{dockerCli: cli} + + cli.EXPECT().Client().Return(apiClient).AnyTimes() + + // First call: Ping fails with a transient error + firstCall := apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}). + Return(client.PingResult{}, context.DeadlineExceeded).Times(1) + + // Second call: Ping succeeds after the transient failure + apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}). + Return(client.PingResult{APIVersion: "1.44"}, nil).Times(1).After(firstCall) + apiClient.EXPECT().ClientVersion().Return("1.44").Times(1) + + // First call should return the transient error + _, err := tested.CurrentAPIVersion(t.Context()) + assert.ErrorIs(t, err, context.DeadlineExceeded) + + // Second call should succeed — error was not cached + version, err := tested.CurrentAPIVersion(t.Context()) + assert.NilError(t, err) + assert.Equal(t, version, "1.44") + + // Third call should return the cached value without calling Ping again + version, err = tested.CurrentAPIVersion(t.Context()) + assert.NilError(t, err) + assert.Equal(t, version, "1.44") +} From 1059a7c3cae7d97c1479a5d5e15eff98b23d976d Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Tue, 31 Mar 2026 11:24:24 +0200 Subject: [PATCH 4/4] refactor: merge RuntimeVersion and CurrentAPIVersion into RuntimeAPIVersion After API negotiation, Compose should only rely on the negotiated version and never use the daemon's raw max version for request shaping. Merge both functions into a single RuntimeAPIVersion that negotiates via Ping and returns ClientVersion, erroring if the client reports an empty version instead of silently falling back to ServerVersion. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Guillaume Lours --- pkg/compose/compose.go | 56 ++++++++++----------------------- pkg/compose/convergence_test.go | 49 +++++++---------------------- pkg/compose/create.go | 4 +-- pkg/compose/images.go | 2 +- 4 files changed, 30 insertions(+), 81 deletions(-) diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index e6fce9be3b8..33ed81af9b8 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -216,8 +216,7 @@ type composeService struct { maxConcurrency int dryRun bool - runtimeVersion runtimeVersionCache - currentAPIVersion runtimeVersionCache + runtimeAPIVersion runtimeVersionCache } // Close releases any connections/resources held by the underlying clients. @@ -502,34 +501,18 @@ type runtimeVersionCache struct { val string } -// RuntimeVersion returns the raw API version reported by the daemon. -// Callers that need the negotiated/effective client API version should use -// CurrentAPIVersion instead. -func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) { - s.runtimeVersion.mu.Lock() - defer s.runtimeVersion.mu.Unlock() - if s.runtimeVersion.val != "" { - return s.runtimeVersion.val, nil - } - version, err := s.apiClient().ServerVersion(ctx, client.ServerVersionOptions{}) - if err != nil { - return "", err - } - s.runtimeVersion.val = version.APIVersion - return s.runtimeVersion.val, nil -} - -// CurrentAPIVersion returns the API version currently used by the Docker client. -// Trigger negotiation first so version-gated request shaping matches the version -// that subsequent API calls will actually use. +// RuntimeAPIVersion returns the negotiated API version that will be used for +// requests to the Docker daemon. It triggers version negotiation via Ping so +// that version-gated request shaping matches the version subsequent API calls +// will actually use. // -// Lock ordering: currentAPIVersion.mu must be acquired before runtimeVersion.mu -// (via the RuntimeVersion fallback). No code path should reverse this order. -func (s *composeService) CurrentAPIVersion(ctx context.Context) (string, error) { - s.currentAPIVersion.mu.Lock() - defer s.currentAPIVersion.mu.Unlock() - if s.currentAPIVersion.val != "" { - return s.currentAPIVersion.val, nil +// After negotiation, Compose should never rely on features or request attributes +// not defined by this API version, even if the daemon's raw version is higher. +func (s *composeService) RuntimeAPIVersion(ctx context.Context) (string, error) { + s.runtimeAPIVersion.mu.Lock() + defer s.runtimeAPIVersion.mu.Unlock() + if s.runtimeAPIVersion.val != "" { + return s.runtimeAPIVersion.val, nil } cli := s.apiClient() @@ -539,17 +522,10 @@ func (s *composeService) CurrentAPIVersion(ctx context.Context) (string, error) } version := cli.ClientVersion() - if version != "" { - s.currentAPIVersion.val = version - return s.currentAPIVersion.val, nil + if version == "" { + return "", fmt.Errorf("docker client returned empty version after successful API negotiation") } - // Defensive fallback for unexpected client implementations or mocks that - // do not populate ClientVersion after a successful negotiated ping. - val, err := s.RuntimeVersion(ctx) - if err != nil { - return "", err - } - s.currentAPIVersion.val = val - return s.currentAPIVersion.val, nil + s.runtimeAPIVersion.val = version + return s.runtimeAPIVersion.val, nil } diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go index 7012576eb3f..27d312c0cb5 100644 --- a/pkg/compose/convergence_test.go +++ b/pkg/compose/convergence_test.go @@ -498,7 +498,7 @@ func TestCreateMobyContainer(t *testing.T) { assert.NilError(t, err) } -func TestCurrentAPIVersionCachesNegotiation(t *testing.T) { +func TestRuntimeAPIVersionCachesNegotiation(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -508,52 +508,25 @@ func TestCurrentAPIVersionCachesNegotiation(t *testing.T) { cli.EXPECT().Client().Return(apiClient).AnyTimes() + // Ping reports the server's max API version (1.44), but after negotiation + // the client may settle on a lower version (1.43) — e.g. when the client + // SDK caps at an older version. RuntimeAPIVersion must return the negotiated + // ClientVersion, not the server's raw APIVersion. apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).Return(client.PingResult{ APIVersion: "1.44", }, nil).Times(1) apiClient.EXPECT().ClientVersion().Return("1.43").Times(1) - version, err := tested.CurrentAPIVersion(t.Context()) + version, err := tested.RuntimeAPIVersion(t.Context()) assert.NilError(t, err) assert.Equal(t, version, "1.43") - version, err = tested.CurrentAPIVersion(t.Context()) + version, err = tested.RuntimeAPIVersion(t.Context()) assert.NilError(t, err) assert.Equal(t, version, "1.43") } -func TestRuntimeVersionRetriesOnTransientError(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - - apiClient := mocks.NewMockAPIClient(mockCtrl) - cli := mocks.NewMockCli(mockCtrl) - tested := &composeService{dockerCli: cli} - - cli.EXPECT().Client().Return(apiClient).AnyTimes() - - // First call: ServerVersion fails with a transient error - firstCall := apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()). - Return(client.ServerVersionResult{}, context.DeadlineExceeded).Times(1) - - // Second call: succeeds - apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()). - Return(client.ServerVersionResult{APIVersion: "1.48"}, nil).Times(1).After(firstCall) - - _, err := tested.RuntimeVersion(t.Context()) - assert.ErrorIs(t, err, context.DeadlineExceeded) - - version, err := tested.RuntimeVersion(t.Context()) - assert.NilError(t, err) - assert.Equal(t, version, "1.48") - - // Third call returns cached value - version, err = tested.RuntimeVersion(t.Context()) - assert.NilError(t, err) - assert.Equal(t, version, "1.48") -} - -func TestCurrentAPIVersionRetriesOnTransientError(t *testing.T) { +func TestRuntimeAPIVersionRetriesOnTransientError(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -573,16 +546,16 @@ func TestCurrentAPIVersionRetriesOnTransientError(t *testing.T) { apiClient.EXPECT().ClientVersion().Return("1.44").Times(1) // First call should return the transient error - _, err := tested.CurrentAPIVersion(t.Context()) + _, err := tested.RuntimeAPIVersion(t.Context()) assert.ErrorIs(t, err, context.DeadlineExceeded) // Second call should succeed — error was not cached - version, err := tested.CurrentAPIVersion(t.Context()) + version, err := tested.RuntimeAPIVersion(t.Context()) assert.NilError(t, err) assert.Equal(t, version, "1.44") // Third call should return the cached value without calling Ping again - version, err = tested.CurrentAPIVersion(t.Context()) + version, err = tested.RuntimeAPIVersion(t.Context()) assert.NilError(t, err) assert.Equal(t, version, "1.44") } diff --git a/pkg/compose/create.go b/pkg/compose/create.go index ce7eced9948..78db3d0fd8c 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -252,7 +252,7 @@ func (s *composeService) getCreateConfigs(ctx context.Context, if err != nil { return createConfigs{}, err } - apiVersion, err := s.CurrentAPIVersion(ctx) + apiVersion, err := s.RuntimeAPIVersion(ctx) if err != nil { return createConfigs{}, err } @@ -899,7 +899,7 @@ func (s *composeService) buildContainerVolumes( case mount.TypeImage: // The daemon validates image mounts against the negotiated API version // from the request path, not the server's own max version. - version, err := s.CurrentAPIVersion(ctx) + version, err := s.RuntimeAPIVersion(ctx) if err != nil { return nil, nil, err } diff --git a/pkg/compose/images.go b/pkg/compose/images.go index 24d37c77f28..d97f1812204 100644 --- a/pkg/compose/images.go +++ b/pkg/compose/images.go @@ -58,7 +58,7 @@ func (s *composeService) Images(ctx context.Context, projectName string, options // The daemon validates the platform field in ImageInspect against the // negotiated API version from the request path, not the server's own max version. - version, err := s.CurrentAPIVersion(ctx) + version, err := s.RuntimeAPIVersion(ctx) if err != nil { return nil, err }