api/types/system: remove deprecated Commit.Expected field

This field was deprecated API v1.48 in [moby@ff191c5], and removed in
API v1.49 in [moby@564abf9].

This patch:

- Removes the field from the API Go types.
- Reimplements the `/info` endpoint with the `compat` package to replace
  the local `infoResponse` implementation.
- Removes the `ServiceConfig.ExtraFields` field in api/types/registry
  introduced in [moby@7d9c50d] to backfill the `AllowNondistributableArtifactsCIDRs`
  and `AllowNondistributableArtifactsHostnames` fields for API < v1.47.

We should also consider deprecating the `ContainerdCommit`, `RuncCommit`
and `InitCommit` fields on the `/info` response (as we also include this
information as part of the components returned in `/version`), but those
can still be useful currently for situations where a user only provides
`docker info` output.

[moby@ff191c5]: ff191c58f7
[moby@564abf9]: 564abf9157
[moby@7d9c50d]: 7d9c50db2b

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-10-07 18:17:23 +02:00
parent b5aae8df00
commit b1e20b6a3a
9 changed files with 54 additions and 191 deletions

View File

@@ -11,9 +11,6 @@ type ServiceConfig struct {
InsecureRegistryCIDRs []netip.Prefix `json:"InsecureRegistryCIDRs"`
IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"`
Mirrors []string
// ExtraFields is for internal use to include deprecated fields on older API versions.
ExtraFields map[string]any `json:"-"`
}
// IndexInfo contains information about a registry

View File

@@ -139,11 +139,6 @@ type PluginsInfo struct {
type Commit struct {
// ID is the actual commit ID or version of external tool.
ID string
// Expected is the commit ID of external tool expected by dockerd as set at build time.
//
// Deprecated: this field is no longer used in API v1.49, but kept for backward-compatibility with older API versions.
Expected string `json:",omitempty"`
}
// NetworkAddressPool is a temp struct used by [Info] struct.

View File

@@ -1,44 +0,0 @@
package system
import (
"encoding/json"
"maps"
"github.com/moby/moby/api/types/system"
)
// infoResponse is a wrapper around [system.Info] with a custom
// marshal function for legacy fields.
type infoResponse struct {
*system.Info
// extraFields is for internal use to include deprecated fields on older API versions.
extraFields map[string]any
}
// MarshalJSON implements a custom marshaler to include legacy fields
// in API responses.
func (ir *infoResponse) MarshalJSON() ([]byte, error) {
type tmp *system.Info
base, err := json.Marshal((tmp)(ir.Info))
if err != nil {
return nil, err
}
if len(ir.extraFields) == 0 && (ir.Info == nil || ir.Info.RegistryConfig == nil || len(ir.Info.RegistryConfig.ExtraFields) == 0) {
return base, nil
}
var merged map[string]any
_ = json.Unmarshal(base, &merged)
// Merge top-level extraFields
maps.Copy(merged, ir.extraFields)
// Merge RegistryConfig.ExtraFields if present
if ir.Info != nil && ir.Info.RegistryConfig != nil && len(ir.Info.RegistryConfig.ExtraFields) > 0 {
if rc, ok := merged["RegistryConfig"].(map[string]any); ok {
maps.Copy(rc, ir.Info.RegistryConfig.ExtraFields)
}
}
return json.Marshal(merged)
}

View File

@@ -1,104 +0,0 @@
package system
import (
"encoding/json"
"strings"
"testing"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/system"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestLegacyFields(t *testing.T) {
infoResp := &infoResponse{
Info: &system.Info{
Containers: 10,
},
extraFields: map[string]any{
"LegacyFoo": false,
"LegacyBar": true,
},
}
data, err := json.MarshalIndent(infoResp, "", " ")
if err != nil {
t.Fatal(err)
}
if expected := `"LegacyFoo": false`; !strings.Contains(string(data), expected) {
t.Errorf("legacy fields should contain %s: %s", expected, string(data))
}
if expected := `"LegacyBar": true`; !strings.Contains(string(data), expected) {
t.Errorf("legacy fields should contain %s: %s", expected, string(data))
}
}
// TestMarshalRegistryConfigLegacyFields verifies extra fields in the registry config
// field in the info response are serialized if they are not empty.
// This is used for backwards compatibility for API versions < 1.47.
func TestMarshalRegistryConfigLegacyFields(t *testing.T) {
expected := []string{"AllowNondistributableArtifactsCIDRs", "AllowNondistributableArtifactsHostnames"}
tests := []struct {
name string
info *infoResponse
assert func(t *testing.T, data []byte, err error)
}{
{
name: "without legacy fields",
info: &infoResponse{
Info: &system.Info{},
},
assert: func(t *testing.T, data []byte, err error) {
assert.NilError(t, err)
var resp map[string]any
err = json.Unmarshal(data, &resp)
assert.NilError(t, err)
rc, ok := resp["RegistryConfig"]
assert.Check(t, ok)
for _, v := range expected {
assert.Check(t, !is.Contains(rc, v)().Success())
}
},
},
{
name: "with legacy fields",
info: &infoResponse{
Info: &system.Info{
RegistryConfig: &registry.ServiceConfig{
ExtraFields: map[string]any{
"AllowNondistributableArtifactsCIDRs": json.RawMessage(nil),
"AllowNondistributableArtifactsHostnames": json.RawMessage(nil),
},
},
},
},
assert: func(t *testing.T, data []byte, err error) {
assert.NilError(t, err)
var resp map[string]any
err = json.Unmarshal(data, &resp)
assert.NilError(t, err)
rc, ok := resp["RegistryConfig"]
assert.Check(t, ok)
for _, v := range expected {
assert.Check(t, is.Contains(rc, v))
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
data, err := json.MarshalIndent(tc.info, "", " ")
tc.assert(t, data, err)
})
}
}

View File

@@ -1,6 +1,7 @@
package system
import (
"github.com/moby/moby/v2/daemon/internal/compat"
"github.com/moby/moby/v2/daemon/server/router"
"resenje.org/singleflight"
)
@@ -17,7 +18,7 @@ type systemRouter struct {
// collectSystemInfo is a single-flight for the /info endpoint,
// unique per API version (as different API versions may return
// a different API response).
collectSystemInfo singleflight.Group[string, *infoResponse]
collectSystemInfo singleflight.Group[string, *compat.Wrapper]
}
// NewRouter initializes a new system router

View File

@@ -64,7 +64,7 @@ func (s *systemRouter) swarmStatus() string {
func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
version := httputils.VersionFromContext(ctx)
info, _, _ := s.collectSystemInfo.Do(ctx, version, func(ctx context.Context) (*infoResponse, error) {
info, _, _ := s.collectSystemInfo.Do(ctx, version, func(ctx context.Context) (*compat.Wrapper, error) {
info, err := s.backend.SystemInfo(ctx)
if err != nil {
return nil, err
@@ -75,6 +75,7 @@ func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *ht
info.Warnings = append(info.Warnings, info.Swarm.Warnings...)
}
var legacyOptions []compat.Option
if versions.LessThan(version, "1.44") {
for k, rt := range info.Runtimes {
// Status field introduced in API v1.44.
@@ -88,35 +89,36 @@ func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *ht
if versions.LessThan(version, "1.47") {
// Field is omitted in API 1.48 and up, but should still be included
// in older versions, even if no values are set.
info.RegistryConfig.ExtraFields = map[string]any{
"AllowNondistributableArtifactsCIDRs": json.RawMessage(nil),
"AllowNondistributableArtifactsHostnames": json.RawMessage(nil),
}
legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
"RegistryConfig": map[string]any{
"AllowNondistributableArtifactsCIDRs": json.RawMessage(nil),
"AllowNondistributableArtifactsHostnames": json.RawMessage(nil),
},
}))
}
if versions.LessThan(version, "1.49") {
// FirewallBackend field introduced in API v1.49.
info.FirewallBackend = nil
}
extraFields := map[string]any{}
if versions.LessThan(version, "1.49") {
// Expected commits are omitted in API 1.49, but should still be
// included in older versions.
info.ContainerdCommit.Expected = info.ContainerdCommit.ID //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
info.RuncCommit.Expected = info.RuncCommit.ID //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
info.InitCommit.Expected = info.InitCommit.ID //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
"ContainerdCommit": map[string]any{"Expected": info.ContainerdCommit.ID},
"RuncCommit": map[string]any{"Expected": info.RuncCommit.ID},
"InitCommit": map[string]any{"Expected": info.InitCommit.ID},
}))
}
if versions.LessThan(version, "1.50") {
info.DiscoveredDevices = nil
// These fields are omitted in > API 1.49, and always false
// older API versions.
extraFields = map[string]any{
legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
"BridgeNfIptables": json.RawMessage("false"),
"BridgeNfIp6tables": json.RawMessage("false"),
}
}))
}
return &infoResponse{Info: info, extraFields: extraFields}, nil
return compat.Wrap(info, legacyOptions...), nil
})
return httputils.WriteJSON(w, http.StatusOK, info)

View File

@@ -8,7 +8,6 @@ import (
"net/http"
"testing"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/internal/testutil/request"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@@ -17,38 +16,63 @@ import (
func TestInfoBinaryCommits(t *testing.T) {
ctx := setupTest(t)
t.Run("current", func(t *testing.T) {
apiClient := testEnv.APIClient()
// API v1.48 and lower returned both the "current" commit (ID) and "expected" commit.
// The "Expected" field has been removed in the API types, so define
// an ad-hoc type for this test.
type legacyCommit struct {
ID string
Expected string
}
info, err := apiClient.Info(ctx)
type legacyInfo struct {
ContainerdCommit legacyCommit
RuncCommit legacyCommit
InitCommit legacyCommit
}
t.Run("current", func(t *testing.T) {
res, body, err := request.Get(ctx, "/info", request.JSON)
assert.NilError(t, err)
assert.Equal(t, res.StatusCode, http.StatusOK)
buf, err := request.ReadBody(body)
assert.NilError(t, err)
var info legacyInfo
err = json.Unmarshal(buf, &info)
assert.NilError(t, err)
assert.Check(t, info.ContainerdCommit.ID != "N/A")
assert.Check(t, is.Equal(info.ContainerdCommit.Expected, "")) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
assert.Check(t, is.Equal(info.ContainerdCommit.Expected, ""))
assert.Check(t, info.InitCommit.ID != "N/A")
assert.Check(t, is.Equal(info.InitCommit.Expected, "")) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
assert.Check(t, is.Equal(info.InitCommit.Expected, ""))
assert.Check(t, info.RuncCommit.ID != "N/A")
assert.Check(t, is.Equal(info.RuncCommit.Expected, "")) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
assert.Check(t, is.Equal(info.RuncCommit.Expected, ""))
})
// Expected commits are omitted in API 1.49, but should still be included in older versions.
t.Run("1.48", func(t *testing.T) {
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.48"))
res, body, err := request.Get(ctx, "/v1.48/info", request.JSON)
assert.NilError(t, err)
assert.Equal(t, res.StatusCode, http.StatusOK)
buf, err := request.ReadBody(body)
assert.NilError(t, err)
info, err := apiClient.Info(ctx)
var info legacyInfo
err = json.Unmarshal(buf, &info)
assert.NilError(t, err)
assert.Check(t, info.ContainerdCommit.ID != "N/A")
assert.Check(t, is.Equal(info.ContainerdCommit.Expected, info.ContainerdCommit.ID)) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
assert.Check(t, is.Equal(info.ContainerdCommit.Expected, info.ContainerdCommit.ID))
assert.Check(t, info.InitCommit.ID != "N/A")
assert.Check(t, is.Equal(info.InitCommit.Expected, info.InitCommit.ID)) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
assert.Check(t, is.Equal(info.InitCommit.Expected, info.InitCommit.ID))
assert.Check(t, info.RuncCommit.ID != "N/A")
assert.Check(t, is.Equal(info.RuncCommit.Expected, info.RuncCommit.ID)) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.49.
assert.Check(t, is.Equal(info.RuncCommit.Expected, info.RuncCommit.ID))
})
}

View File

@@ -11,9 +11,6 @@ type ServiceConfig struct {
InsecureRegistryCIDRs []netip.Prefix `json:"InsecureRegistryCIDRs"`
IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"`
Mirrors []string
// ExtraFields is for internal use to include deprecated fields on older API versions.
ExtraFields map[string]any `json:"-"`
}
// IndexInfo contains information about a registry

View File

@@ -139,11 +139,6 @@ type PluginsInfo struct {
type Commit struct {
// ID is the actual commit ID or version of external tool.
ID string
// Expected is the commit ID of external tool expected by dockerd as set at build time.
//
// Deprecated: this field is no longer used in API v1.49, but kept for backward-compatibility with older API versions.
Expected string `json:",omitempty"`
}
// NetworkAddressPool is a temp struct used by [Info] struct.