From 9fc12daf80f023c3b42b24d9e18daf73f9e2096f Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 18 Sep 2025 15:39:12 +0200 Subject: [PATCH 1/4] client: remove version-gate for "--force" on "volume remove" The `force` option on volume remove was added in [moby@6c5c34d] (docker 1.13.0-rc1, API v1.25), but did not gate the feature to API version, so effectively introduced it to all existing API versions. After this, [moby@e98e4a7] enabled experimental features by default, and added API version gates, but only did so on the client side, so the daemon / API server would continue to accept the `force` option on any API version. Let's remove this code, given that: - API v1.24 is the oldest API version we still handle, and only as fallback. - This code silently discards the user's option (no warning / error) - Every current version of the daemon handles the option, regardless of API version (only a 9+ year old daemon wouldn't handle it). [moby@6c5c34d]: https://github.com/moby/moby/commit/6c5c34d50d377d1c5318a255240fb2dc9c23cf92 [moby@e98e4a7]: https://github.com/moby/moby/commit/e98e4a71110fd33852bb755a9b8b4ebc9df904db Signed-off-by: Sebastiaan van Stijn --- client/volume_remove.go | 14 +------------- client/volume_remove_test.go | 7 ++++++- .../github.com/moby/moby/client/volume_remove.go | 14 +------------- 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/client/volume_remove.go b/client/volume_remove.go index c165b22f4e..7fcd36e0ec 100644 --- a/client/volume_remove.go +++ b/client/volume_remove.go @@ -3,8 +3,6 @@ package client import ( "context" "net/url" - - "github.com/moby/moby/api/types/versions" ) // VolumeRemove removes a volume from the docker host. @@ -16,17 +14,7 @@ func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool query := url.Values{} if force { - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return err - } - if versions.GreaterThanOrEqualTo(cli.version, "1.25") { - query.Set("force", "1") - } + query.Set("force", "1") } resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) defer ensureReaderClosed(resp) diff --git a/client/volume_remove_test.go b/client/volume_remove_test.go index ad50afdae8..037988dcfe 100644 --- a/client/volume_remove_test.go +++ b/client/volume_remove_test.go @@ -3,6 +3,7 @@ package client import ( "bytes" "context" + "fmt" "io" "net/http" "testing" @@ -47,6 +48,10 @@ func TestVolumeRemove(t *testing.T) { if err := assertRequest(req, http.MethodDelete, expectedURL); err != nil { return nil, err } + if v := req.URL.Query().Get("force"); v != "1" { + return nil, fmt.Errorf("expected force=1, got %s", v) + } + return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("body"))), @@ -54,6 +59,6 @@ func TestVolumeRemove(t *testing.T) { })) assert.NilError(t, err) - err = client.VolumeRemove(context.Background(), "volume_id", false) + err = client.VolumeRemove(context.Background(), "volume_id", true) assert.NilError(t, err) } diff --git a/vendor/github.com/moby/moby/client/volume_remove.go b/vendor/github.com/moby/moby/client/volume_remove.go index c165b22f4e..7fcd36e0ec 100644 --- a/vendor/github.com/moby/moby/client/volume_remove.go +++ b/vendor/github.com/moby/moby/client/volume_remove.go @@ -3,8 +3,6 @@ package client import ( "context" "net/url" - - "github.com/moby/moby/api/types/versions" ) // VolumeRemove removes a volume from the docker host. @@ -16,17 +14,7 @@ func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool query := url.Values{} if force { - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return err - } - if versions.GreaterThanOrEqualTo(cli.version, "1.25") { - query.Set("force", "1") - } + query.Set("force", "1") } resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) defer ensureReaderClosed(resp) From 0673d43663da9f17014c9749d6566ecb41241830 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 18 Sep 2025 16:38:45 +0200 Subject: [PATCH 2/4] client: remove "version" header for service create, update The version header is no longer used since [moby@a9d2091] (v20.10.0-beta1) which was not gated by API version, as handling of the header was broken (using the client version, instead of the API version used for the request). Given that any current version of the daemon, regardless of API version will ignore the header, this code was only in place to allow connecting to a daemon older than (v20.10.0-beta1), which would be long EOL now. [moby@a9d2091]: https://github.com/moby/moby/commit/a9d20916c3d1f5e3c3ddada79af19aee4ec64629 Signed-off-by: Sebastiaan van Stijn --- client/service_create.go | 6 ------ client/service_update.go | 7 ------- vendor/github.com/moby/moby/client/service_create.go | 6 ------ vendor/github.com/moby/moby/client/service_update.go | 7 ------- 4 files changed, 26 deletions(-) diff --git a/client/service_create.go b/client/service_create.go index c98f6f1a5f..f49c3e6f8b 100644 --- a/client/service_create.go +++ b/client/service_create.go @@ -62,12 +62,6 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, } headers := http.Header{} - if versions.LessThan(cli.version, "1.30") { - // the custom "version" header was used by engine API before 20.10 - // (API 1.30) to switch between client- and server-side lookup of - // image digests. - headers["version"] = []string{cli.version} - } if options.EncodedRegistryAuth != "" { headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth} } diff --git a/client/service_update.go b/client/service_update.go index 0449c33161..b9d8ae2cab 100644 --- a/client/service_update.go +++ b/client/service_update.go @@ -8,7 +8,6 @@ import ( "github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/swarm" - "github.com/moby/moby/api/types/versions" ) // ServiceUpdate updates a Service. The version number is required to avoid @@ -65,12 +64,6 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version } headers := http.Header{} - if versions.LessThan(cli.version, "1.30") { - // the custom "version" header was used by engine API before 20.10 - // (API 1.30) to switch between client- and server-side lookup of - // image digests. - headers["version"] = []string{cli.version} - } if options.EncodedRegistryAuth != "" { headers.Set(registry.AuthHeader, options.EncodedRegistryAuth) } diff --git a/vendor/github.com/moby/moby/client/service_create.go b/vendor/github.com/moby/moby/client/service_create.go index c98f6f1a5f..f49c3e6f8b 100644 --- a/vendor/github.com/moby/moby/client/service_create.go +++ b/vendor/github.com/moby/moby/client/service_create.go @@ -62,12 +62,6 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, } headers := http.Header{} - if versions.LessThan(cli.version, "1.30") { - // the custom "version" header was used by engine API before 20.10 - // (API 1.30) to switch between client- and server-side lookup of - // image digests. - headers["version"] = []string{cli.version} - } if options.EncodedRegistryAuth != "" { headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth} } diff --git a/vendor/github.com/moby/moby/client/service_update.go b/vendor/github.com/moby/moby/client/service_update.go index 0449c33161..b9d8ae2cab 100644 --- a/vendor/github.com/moby/moby/client/service_update.go +++ b/vendor/github.com/moby/moby/client/service_update.go @@ -8,7 +8,6 @@ import ( "github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/swarm" - "github.com/moby/moby/api/types/versions" ) // ServiceUpdate updates a Service. The version number is required to avoid @@ -65,12 +64,6 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version } headers := http.Header{} - if versions.LessThan(cli.version, "1.30") { - // the custom "version" header was used by engine API before 20.10 - // (API 1.30) to switch between client- and server-side lookup of - // image digests. - headers["version"] = []string{cli.version} - } if options.EncodedRegistryAuth != "" { headers.Set(registry.AuthHeader, options.EncodedRegistryAuth) } From d60b4ea278e4846663a538214591c98e8a0e9ee9 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 18 Sep 2025 17:22:07 +0200 Subject: [PATCH 3/4] client: fix version-gate for readonly-recursive mounts validation on service commit [moby@5d6b566] migrated this validation from the CLI to the client, but for some reason picked the wrong API version inside ServiceCreate. The CLI code was added to an existing validation, which only handled validation when creating a service, but not when updating, which meant that adding this option to an existing service would not invalidate it. This patch: - moves the version-gate to the validation code - merges validateServiceSpecForAPIVersion into validateServiceSpec, to keep the validation combined, and to make sure validation happens both on create and update. [moby@5d6b566]: https://github.com/moby/moby/commit/5d6b56699deac829bec841180b36f4fbbd469fc7 Signed-off-by: Sebastiaan van Stijn --- client/service_create.go | 31 +++++++------------ client/service_update.go | 7 ++--- .../moby/moby/client/service_create.go | 31 +++++++------------ .../moby/moby/client/service_update.go | 7 ++--- 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/client/service_create.go b/client/service_create.go index f49c3e6f8b..0f1bb60eec 100644 --- a/client/service_create.go +++ b/client/service_create.go @@ -33,14 +33,9 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{} } - if err := validateServiceSpec(service); err != nil { + if err := validateServiceSpec(service, cli.version); err != nil { return response, err } - if versions.LessThan(cli.version, "1.30") { - if err := validateServiceSpecForAPIVersion(service, cli.version); err != nil { - return response, err - } - } // ensure that the image is tagged var resolveWarning string @@ -177,7 +172,7 @@ func digestWarning(image string) string { return fmt.Sprintf("image %s could not be accessed on a registry to record\nits digest. Each node will access %s independently,\npossibly leading to different nodes running different\nversions of the image.\n", image, image) } -func validateServiceSpec(s swarm.ServiceSpec) error { +func validateServiceSpec(s swarm.ServiceSpec, apiVersion string) error { if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil { return errors.New("must not specify both a container spec and a plugin spec in the task template") } @@ -187,18 +182,16 @@ func validateServiceSpec(s swarm.ServiceSpec) error { if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) { return errors.New("mismatched runtime with container spec") } - return nil -} - -func validateServiceSpecForAPIVersion(c swarm.ServiceSpec, apiVersion string) error { - for _, m := range c.TaskTemplate.ContainerSpec.Mounts { - if m.BindOptions != nil { - if m.BindOptions.NonRecursive && versions.LessThan(apiVersion, "1.40") { - return errors.New("bind-recursive=disabled requires API v1.40 or later") - } - // ReadOnlyNonRecursive can be safely ignored when API < 1.44 - if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(apiVersion, "1.44") { - return errors.New("bind-recursive=readonly requires API v1.44 or later") + if s.TaskTemplate.ContainerSpec != nil && apiVersion != "" && versions.LessThan(apiVersion, "1.44") { + for _, m := range s.TaskTemplate.ContainerSpec.Mounts { + if m.BindOptions != nil { + if m.BindOptions.NonRecursive && versions.LessThan(apiVersion, "1.40") { + return errors.New("bind-recursive=disabled requires API v1.40 or later") + } + // ReadOnlyNonRecursive can be safely ignored when API < 1.44 + if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(apiVersion, "1.44") { + return errors.New("bind-recursive=readonly requires API v1.44 or later") + } } } } diff --git a/client/service_update.go b/client/service_update.go index b9d8ae2cab..b253e8605b 100644 --- a/client/service_update.go +++ b/client/service_update.go @@ -28,6 +28,9 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version if err := cli.checkVersion(ctx); err != nil { return swarm.ServiceUpdateResponse{}, err } + if err := validateServiceSpec(service, cli.version); err != nil { + return swarm.ServiceUpdateResponse{}, err + } query := url.Values{} if options.RegistryAuthFrom != "" { @@ -40,10 +43,6 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version query.Set("version", version.String()) - if err := validateServiceSpec(service); err != nil { - return swarm.ServiceUpdateResponse{}, err - } - // ensure that the image is tagged var resolveWarning string switch { diff --git a/vendor/github.com/moby/moby/client/service_create.go b/vendor/github.com/moby/moby/client/service_create.go index f49c3e6f8b..0f1bb60eec 100644 --- a/vendor/github.com/moby/moby/client/service_create.go +++ b/vendor/github.com/moby/moby/client/service_create.go @@ -33,14 +33,9 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{} } - if err := validateServiceSpec(service); err != nil { + if err := validateServiceSpec(service, cli.version); err != nil { return response, err } - if versions.LessThan(cli.version, "1.30") { - if err := validateServiceSpecForAPIVersion(service, cli.version); err != nil { - return response, err - } - } // ensure that the image is tagged var resolveWarning string @@ -177,7 +172,7 @@ func digestWarning(image string) string { return fmt.Sprintf("image %s could not be accessed on a registry to record\nits digest. Each node will access %s independently,\npossibly leading to different nodes running different\nversions of the image.\n", image, image) } -func validateServiceSpec(s swarm.ServiceSpec) error { +func validateServiceSpec(s swarm.ServiceSpec, apiVersion string) error { if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil { return errors.New("must not specify both a container spec and a plugin spec in the task template") } @@ -187,18 +182,16 @@ func validateServiceSpec(s swarm.ServiceSpec) error { if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) { return errors.New("mismatched runtime with container spec") } - return nil -} - -func validateServiceSpecForAPIVersion(c swarm.ServiceSpec, apiVersion string) error { - for _, m := range c.TaskTemplate.ContainerSpec.Mounts { - if m.BindOptions != nil { - if m.BindOptions.NonRecursive && versions.LessThan(apiVersion, "1.40") { - return errors.New("bind-recursive=disabled requires API v1.40 or later") - } - // ReadOnlyNonRecursive can be safely ignored when API < 1.44 - if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(apiVersion, "1.44") { - return errors.New("bind-recursive=readonly requires API v1.44 or later") + if s.TaskTemplate.ContainerSpec != nil && apiVersion != "" && versions.LessThan(apiVersion, "1.44") { + for _, m := range s.TaskTemplate.ContainerSpec.Mounts { + if m.BindOptions != nil { + if m.BindOptions.NonRecursive && versions.LessThan(apiVersion, "1.40") { + return errors.New("bind-recursive=disabled requires API v1.40 or later") + } + // ReadOnlyNonRecursive can be safely ignored when API < 1.44 + if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(apiVersion, "1.44") { + return errors.New("bind-recursive=readonly requires API v1.44 or later") + } } } } diff --git a/vendor/github.com/moby/moby/client/service_update.go b/vendor/github.com/moby/moby/client/service_update.go index b9d8ae2cab..b253e8605b 100644 --- a/vendor/github.com/moby/moby/client/service_update.go +++ b/vendor/github.com/moby/moby/client/service_update.go @@ -28,6 +28,9 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version if err := cli.checkVersion(ctx); err != nil { return swarm.ServiceUpdateResponse{}, err } + if err := validateServiceSpec(service, cli.version); err != nil { + return swarm.ServiceUpdateResponse{}, err + } query := url.Values{} if options.RegistryAuthFrom != "" { @@ -40,10 +43,6 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version query.Set("version", version.String()) - if err := validateServiceSpec(service); err != nil { - return swarm.ServiceUpdateResponse{}, err - } - // ensure that the image is tagged var resolveWarning string switch { From f7ed1b84d23d495b151321ab279206c557f104c5 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 18 Sep 2025 18:00:30 +0200 Subject: [PATCH 4/4] client: ImageList: don't discard reference filter on API < 1.25 the "reference" filter was introduced in [moby@820b809] (docker 1.13.0-rc1) to replace the "filter" query argument. That commit initially included a version-gate anticipating the API version to be used for v17.12, but as this was yet unknown, the version-gate was removed in [moby@0f9d22c]. A later PR re-introduced a version-gate in [moby@4a19009], reflecting the API version in which the deprecation was (finally) completed. For the client, [moby@c6e3145] added a fallback was added for older daemons (docker 1.12.0 and older, using API < v1.25) that did not support the new filter. Looking at the above, any version of docker 1.13.0 or above handles the "reference" filter, but (depending on the docker version) may also handle the old filter on API < 1.28 or API < 1.41. Removing this option will only impact daemon versions older than 1.13.0, which are long obsolete. Given that current clients forcibly remove the "reference" filter and replace it with the old "filter" when using API v1.24, we keep support on the daemon side, but update the version to v1.24, and only if no reference filter is set. [moby@820b809]: https://github.com/moby/moby/commit/820b809e70df8b9c7af00256182c48d935972a5c [moby@c6e3145]: https://github.com/moby/moby/commit/c6e31454ba2f053bc6831651663cf538d142afaa [moby@0f9d22c]: https://github.com/moby/moby/commit/0f9d22cd66353b3d14dd4a08084f88778fb69480 [moby@4a19009]: https://github.com/moby/moby/commit/4a1900915a2eb80f7eaf3638ed702b5d80bfa80f Signed-off-by: Sebastiaan van Stijn --- client/image_list.go | 12 +----- client/image_list_test.go | 39 ------------------- daemon/server/router/image/image_routes.go | 8 ++-- .../github.com/moby/moby/client/image_list.go | 12 +----- 4 files changed, 9 insertions(+), 62 deletions(-) diff --git a/client/image_list.go b/client/image_list.go index 6ca906c5ad..8001d10332 100644 --- a/client/image_list.go +++ b/client/image_list.go @@ -30,16 +30,8 @@ func (cli *Client) ImageList(ctx context.Context, options ImageListOptions) ([]i query := url.Values{} - optionFilters := options.Filters - referenceFilters := optionFilters.Get("reference") - if versions.LessThan(cli.version, "1.25") && len(referenceFilters) > 0 { - query.Set("filter", referenceFilters[0]) - for _, filterValue := range referenceFilters { - optionFilters.Del("reference", filterValue) - } - } - if optionFilters.Len() > 0 { - filterJSON, err := filters.ToJSON(optionFilters) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToJSON(options.Filters) if err != nil { return images, err } diff --git a/client/image_list_test.go b/client/image_list_test.go index 3c860f2ccc..7ac3d0df38 100644 --- a/client/image_list_test.go +++ b/client/image_list_test.go @@ -114,45 +114,6 @@ func TestImageList(t *testing.T) { } } -func TestImageListApiBefore125(t *testing.T) { - expectedFilter := "image:tag" - client, err := NewClientWithOpts(WithMockClient(func(req *http.Request) (*http.Response, error) { - query := req.URL.Query() - actualFilter := query.Get("filter") - if actualFilter != expectedFilter { - return nil, fmt.Errorf("filter not set in URL query properly. Expected '%s', got %s", expectedFilter, actualFilter) - } - actualFilters := query.Get("filters") - if actualFilters != "" { - return nil, fmt.Errorf("filters should have not been present, were with value: %s", actualFilters) - } - content, err := json.Marshal([]image.Summary{ - { - ID: "image_id2", - }, - { - ID: "image_id2", - }, - }) - if err != nil { - return nil, err - } - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(content)), - }, nil - }), WithVersion("1.24")) - assert.NilError(t, err) - - options := ImageListOptions{ - Filters: filters.NewArgs(filters.Arg("reference", "image:tag")), - } - - images, err := client.ImageList(context.Background(), options) - assert.NilError(t, err) - assert.Check(t, is.Len(images, 2)) -} - // Checks if shared-size query parameter is set/not being set correctly // for /images/json. func TestImageListWithSharedSize(t *testing.T) { diff --git a/daemon/server/router/image/image_routes.go b/daemon/server/router/image/image_routes.go index 1af0e13fd2..7dadc93962 100644 --- a/daemon/server/router/image/image_routes.go +++ b/daemon/server/router/image/image_routes.go @@ -427,10 +427,12 @@ func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, } version := httputils.VersionFromContext(ctx) - if versions.LessThan(version, "1.41") { + + // clients may be actively removing the new filter on API 1.24 + // and under: https://github.com/moby/moby/blob/v28.4.0/client/image_list.go#L34-L40 + if versions.LessThan(version, "1.25") && !imageFilters.Contains("reference") { // NOTE: filter is a shell glob string applied to repository names. - filterParam := r.Form.Get("filter") - if filterParam != "" { + if filterParam := r.Form.Get("filter"); filterParam != "" { imageFilters.Add("reference", filterParam) } } diff --git a/vendor/github.com/moby/moby/client/image_list.go b/vendor/github.com/moby/moby/client/image_list.go index 6ca906c5ad..8001d10332 100644 --- a/vendor/github.com/moby/moby/client/image_list.go +++ b/vendor/github.com/moby/moby/client/image_list.go @@ -30,16 +30,8 @@ func (cli *Client) ImageList(ctx context.Context, options ImageListOptions) ([]i query := url.Values{} - optionFilters := options.Filters - referenceFilters := optionFilters.Get("reference") - if versions.LessThan(cli.version, "1.25") && len(referenceFilters) > 0 { - query.Set("filter", referenceFilters[0]) - for _, filterValue := range referenceFilters { - optionFilters.Del("reference", filterValue) - } - } - if optionFilters.Len() > 0 { - filterJSON, err := filters.ToJSON(optionFilters) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToJSON(options.Filters) if err != nil { return images, err }