Browse code

API: /info: remove BridgeNfIptables, BridgeNfIp6tables fields

The `BridgeNfIptables` and `BridgeNfIp6tables` fields in the
`GET /info` response were deprecated in API v1.48, and are now omitted
in API v1.50.

With this patch, old API version continue to return the field:

curl -s --unix-socket /var/run/docker.sock http://localhost/v1.48/info | jq .BridgeNfIp6tables
false

curl -s --unix-socket /var/run/docker.sock http://localhost/v1.48/info | jq .BridgeNfIptables
false

Omitting the field in API v1.50 and above

curl -s --unix-socket /var/run/docker.sock http://localhost/v1.50/info | jq .BridgeNfIp6tables
null

curl -s --unix-socket /var/run/docker.sock http://localhost/v1.50/info | jq .BridgeNfIptables
null

This reverts commit eacbbdeec68779be81983f61f5de5f91d52f656a, and re-applies
a variant of 5d2006256f15f7252c11bd72d632de26a8b2ff06

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/04/30 22:44:08
Showing 8 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,39 @@
0
+// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
1
+//go:build go1.23
2
+
3
+package system
4
+
5
+import (
6
+	"encoding/json"
7
+
8
+	"github.com/docker/docker/api/types/system"
9
+)
10
+
11
+// infoResponse is a wrapper around [system.Info] with a custom
12
+// marshal function for legacy fields.
13
+type infoResponse struct {
14
+	*system.Info
15
+
16
+	// extraFields is for internal use to include deprecated fields on older API versions.
17
+	extraFields map[string]any
18
+}
19
+
20
+// MarshalJSON implements a custom marshaler to include legacy fields
21
+// in API responses.
22
+func (sc *infoResponse) MarshalJSON() ([]byte, error) {
23
+	type tmp *system.Info
24
+	base, err := json.Marshal((tmp)(sc.Info))
25
+	if err != nil {
26
+		return nil, err
27
+	}
28
+	if len(sc.extraFields) == 0 {
29
+		return base, nil
30
+	}
31
+	var merged map[string]any
32
+	_ = json.Unmarshal(base, &merged)
33
+
34
+	for k, v := range sc.extraFields {
35
+		merged[k] = v
36
+	}
37
+	return json.Marshal(merged)
38
+}
0 39
new file mode 100644
... ...
@@ -0,0 +1,33 @@
0
+package system
1
+
2
+import (
3
+	"encoding/json"
4
+	"strings"
5
+	"testing"
6
+
7
+	"github.com/docker/docker/api/types/system"
8
+)
9
+
10
+func TestLegacyFields(t *testing.T) {
11
+	infoResp := &infoResponse{
12
+		Info: &system.Info{
13
+			Containers: 10,
14
+		},
15
+		extraFields: map[string]any{
16
+			"LegacyFoo": false,
17
+			"LegacyBar": true,
18
+		},
19
+	}
20
+
21
+	data, err := json.MarshalIndent(infoResp, "", "  ")
22
+	if err != nil {
23
+		t.Fatal(err)
24
+	}
25
+
26
+	if expected := `"LegacyFoo": false`; !strings.Contains(string(data), expected) {
27
+		t.Errorf("legacy fields should contain %s: %s", expected, string(data))
28
+	}
29
+	if expected := `"LegacyBar": true`; !strings.Contains(string(data), expected) {
30
+		t.Errorf("legacy fields should contain %s: %s", expected, string(data))
31
+	}
32
+}
... ...
@@ -5,7 +5,6 @@ package system // import "github.com/docker/docker/api/server/router/system"
5 5
 
6 6
 import (
7 7
 	"github.com/docker/docker/api/server/router"
8
-	"github.com/docker/docker/api/types/system"
9 8
 	"resenje.org/singleflight"
10 9
 )
11 10
 
... ...
@@ -21,7 +20,7 @@ type systemRouter struct {
21 21
 	// collectSystemInfo is a single-flight for the /info endpoint,
22 22
 	// unique per API version (as different API versions may return
23 23
 	// a different API response).
24
-	collectSystemInfo singleflight.Group[string, *system.Info]
24
+	collectSystemInfo singleflight.Group[string, *infoResponse]
25 25
 }
26 26
 
27 27
 // NewRouter initializes a new system router
... ...
@@ -63,7 +63,7 @@ func (s *systemRouter) swarmStatus() string {
63 63
 
64 64
 func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
65 65
 	version := httputils.VersionFromContext(ctx)
66
-	info, _, _ := s.collectSystemInfo.Do(ctx, version, func(ctx context.Context) (*system.Info, error) {
66
+	info, _, _ := s.collectSystemInfo.Do(ctx, version, func(ctx context.Context) (*infoResponse, error) {
67 67
 		info, err := s.backend.SystemInfo(ctx)
68 68
 		if err != nil {
69 69
 			return nil, err
... ...
@@ -117,6 +117,7 @@ func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *ht
117 117
 			info.FirewallBackend = nil
118 118
 		}
119 119
 
120
+		extraFields := map[string]any{}
120 121
 		if versions.LessThan(version, "1.49") {
121 122
 			// Expected commits are omitted in API 1.49, but should still be
122 123
 			// included in older versions.
... ...
@@ -129,9 +130,17 @@ func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *ht
129 129
 		}
130 130
 		if versions.LessThan(version, "1.50") {
131 131
 			info.DiscoveredDevices = nil
132
+
133
+			// These fields are omitted in > API 1.49, and always false
134
+			// older API versions.
135
+			extraFields = map[string]any{
136
+				"BridgeNfIptables":  json.RawMessage("false"),
137
+				"BridgeNfIp6tables": json.RawMessage("false"),
138
+			}
132 139
 		}
133
-		return info, nil
140
+		return &infoResponse{Info: info, extraFields: extraFields}, nil
134 141
 	})
142
+
135 143
 	return httputils.WriteJSON(w, http.StatusOK, info)
136 144
 }
137 145
 
... ...
@@ -29,8 +29,6 @@ type Info struct {
29 29
 	CPUSet             bool
30 30
 	PidsLimit          bool
31 31
 	IPv4Forwarding     bool
32
-	BridgeNfIptables   bool `json:"BridgeNfIptables"`  // Deprecated: netfilter module is now loaded on-demand and no longer during daemon startup, making this field obsolete. This field is always false and will be removed in the next release.
33
-	BridgeNfIP6tables  bool `json:"BridgeNfIp6tables"` // Deprecated: netfilter module is now loaded on-demand and no longer during daemon startup, making this field obsolete. This field is always false and will be removed in the next release.
34 32
 	Debug              bool
35 33
 	NFd                int
36 34
 	OomKillDisable     bool
... ...
@@ -21,6 +21,9 @@ keywords: "API, Docker, rcli, REST, documentation"
21 21
   `DeviceInfo` objects, each providing details about a device discovered by a
22 22
   device driver.
23 23
   Currently only the CDI device driver is supported.
24
+* Deprecated: The `BridgeNfIptables` and `BridgeNfIp6tables` fields in the
25
+  `GET /info` response were deprecated in API v1.48, and are now omitted
26
+  in API v1.50.
24 27
 
25 28
 ## v1.49 API changes
26 29
 
... ...
@@ -3,9 +3,13 @@
3 3
 package system // import "github.com/docker/docker/integration/system"
4 4
 
5 5
 import (
6
+	"encoding/json"
7
+	"io"
8
+	"net/http"
6 9
 	"testing"
7 10
 
8 11
 	"github.com/docker/docker/client"
12
+	"github.com/docker/docker/testutil/request"
9 13
 	"gotest.tools/v3/assert"
10 14
 	is "gotest.tools/v3/assert/cmp"
11 15
 )
... ...
@@ -47,3 +51,56 @@ func TestInfoBinaryCommits(t *testing.T) {
47 47
 		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.
48 48
 	})
49 49
 }
50
+
51
+func TestInfoLegacyFields(t *testing.T) {
52
+	ctx := setupTest(t)
53
+
54
+	const notPresent = "expected field to not be present"
55
+
56
+	tests := []struct {
57
+		name           string
58
+		url            string
59
+		expectedFields map[string]any
60
+	}{
61
+		{
62
+			name: "api v1.49 legacy bridge-nftables",
63
+			url:  "/v1.49/info",
64
+			expectedFields: map[string]any{
65
+				"BridgeNfIp6tables": false,
66
+				"BridgeNfIptables":  false,
67
+			},
68
+		},
69
+		{
70
+			name: "api v1.50 legacy bridge-nftables",
71
+			url:  "/v1.50/info",
72
+			expectedFields: map[string]any{
73
+				"BridgeNfIp6tables": notPresent,
74
+				"BridgeNfIptables":  notPresent,
75
+			},
76
+		},
77
+	}
78
+	for _, tc := range tests {
79
+		t.Run(tc.name, func(t *testing.T) {
80
+			res, _, err := request.Get(ctx, tc.url)
81
+			assert.NilError(t, err)
82
+			assert.Equal(t, res.StatusCode, http.StatusOK)
83
+			body, err := io.ReadAll(res.Body)
84
+			assert.NilError(t, err)
85
+
86
+			actual := map[string]any{}
87
+			err = json.Unmarshal(body, &actual)
88
+			assert.NilError(t, err, string(body))
89
+
90
+			for field, expectedValue := range tc.expectedFields {
91
+				if expectedValue == notPresent {
92
+					_, found := actual[field]
93
+					assert.Assert(t, !found, "field %s should not be present", field)
94
+				} else {
95
+					_, found := actual[field]
96
+					assert.Assert(t, found, "field %s should be present", field)
97
+					assert.Check(t, is.DeepEqual(actual[field], expectedValue))
98
+				}
99
+			}
100
+		})
101
+	}
102
+}
... ...
@@ -24,16 +24,6 @@ type SysInfo struct {
24 24
 	// Whether IPv4 forwarding is supported or not, if this was disabled, networking will not work
25 25
 	IPv4ForwardingDisabled bool
26 26
 
27
-	// Whether bridge-nf-call-iptables is supported or not
28
-	//
29
-	// Deprecated: netfilter module is now loaded on-demand and no longer during daemon startup, making this field obsolete. This field is always false and will be removed in the next release.
30
-	BridgeNFCallIPTablesDisabled bool
31
-
32
-	// Whether bridge-nf-call-ip6tables is supported or not
33
-	//
34
-	// Deprecated: netfilter module is now loaded on-demand and no longer during daemon startup, making this field obsolete. This field is always false and will be removed in the next release.
35
-	BridgeNFCallIP6TablesDisabled bool
36
-
37 27
 	// Whether the cgroup has the mountpoint of "devices" or not
38 28
 	CgroupDevicesEnabled bool
39 29