Browse code

Don't create endpoint config for MAC addr config migration

In a container-create API request, HostConfig.NetworkMode (the identity
of the "main" network) may be a name, id or short-id.

The configuration for that network, including preferred IP address etc,
may be keyed on network name or id - it need not match the NetworkMode.

So, when migrating the old container-wide MAC address to the new
per-endpoint field - it is not safe to create a new EndpointSettings
entry unless there is no possibility that it will duplicate settings
intended for the same network (because one of the duplicates will be
discarded later, dropping the settings it contains).

This change introduces a new API restriction, if the deprecated container
wide field is used in the new API, and EndpointsConfig is provided for
any network, the NetworkMode and key under which the EndpointsConfig is
store must be the same - no mixing of ids and names.

Signed-off-by: Rob Murray <rob.murray@docker.com>

Rob Murray authored on 2024/02/29 05:06:29
Showing 3 changed files
... ...
@@ -623,42 +623,73 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
623 623
 // networkingConfig to set the endpoint-specific MACAddress field introduced in API v1.44. It returns a warning message
624 624
 // or an error if the container-wide field was specified for API >= v1.44.
625 625
 func handleMACAddressBC(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, version string) (string, error) {
626
-	if config.MacAddress == "" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
627
-		return "", nil
628
-	}
629
-
630 626
 	deprecatedMacAddress := config.MacAddress //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
631 627
 
628
+	// For older versions of the API, migrate the container-wide MAC address to EndpointsConfig.
632 629
 	if versions.LessThan(version, "1.44") {
633
-		// The container-wide MacAddress parameter is deprecated and should now be specified in EndpointsConfig.
634
-		if hostConfig.NetworkMode.IsDefault() || hostConfig.NetworkMode.IsBridge() || hostConfig.NetworkMode.IsUserDefined() {
635
-			nwName := hostConfig.NetworkMode.NetworkName()
636
-			if _, ok := networkingConfig.EndpointsConfig[nwName]; !ok {
637
-				networkingConfig.EndpointsConfig[nwName] = &network.EndpointSettings{}
630
+		if deprecatedMacAddress == "" {
631
+			// If a MAC address is supplied in EndpointsConfig, discard it because the old API
632
+			// would have ignored it.
633
+			for _, ep := range networkingConfig.EndpointsConfig {
634
+				ep.MacAddress = ""
638 635
 			}
639
-			// Overwrite the config: either the endpoint's MacAddress was set by the user on API < v1.44, which
640
-			// must be ignored, or migrate the top-level MacAddress to the endpoint's config.
641
-			networkingConfig.EndpointsConfig[nwName].MacAddress = deprecatedMacAddress
636
+			return "", nil
642 637
 		}
643 638
 		if !hostConfig.NetworkMode.IsDefault() && !hostConfig.NetworkMode.IsBridge() && !hostConfig.NetworkMode.IsUserDefined() {
644 639
 			return "", runconfig.ErrConflictContainerNetworkAndMac
645 640
 		}
646 641
 
642
+		// There cannot be more than one entry in EndpointsConfig with API < 1.44.
643
+
644
+		// If there's no EndpointsConfig, create a place to store the configured address. It is
645
+		// safe to use NetworkMode as the network name, whether it's a name or id/short-id, as
646
+		// it will be normalised later and there is no other EndpointSettings object that might
647
+		// refer to this network/endpoint.
648
+		if len(networkingConfig.EndpointsConfig) == 0 {
649
+			nwName := hostConfig.NetworkMode.NetworkName()
650
+			networkingConfig.EndpointsConfig[nwName] = &network.EndpointSettings{}
651
+		}
652
+		// There's exactly one network in EndpointsConfig, either from the API or just-created.
653
+		// Migrate the container-wide setting to it.
654
+		// No need to check for a match between NetworkMode and the names/ids in EndpointsConfig,
655
+		// the old version of the API would have applied the address to this network anyway.
656
+		for _, ep := range networkingConfig.EndpointsConfig {
657
+			ep.MacAddress = deprecatedMacAddress
658
+		}
647 659
 		return "", nil
648 660
 	}
649 661
 
662
+	// The container-wide MacAddress parameter is deprecated and should now be specified in EndpointsConfig.
663
+	if deprecatedMacAddress == "" {
664
+		return "", nil
665
+	}
650 666
 	var warning string
651 667
 	if hostConfig.NetworkMode.IsDefault() || hostConfig.NetworkMode.IsBridge() || hostConfig.NetworkMode.IsUserDefined() {
652 668
 		nwName := hostConfig.NetworkMode.NetworkName()
653
-		if _, ok := networkingConfig.EndpointsConfig[nwName]; !ok {
654
-			networkingConfig.EndpointsConfig[nwName] = &network.EndpointSettings{}
655
-		}
656
-
657
-		ep := networkingConfig.EndpointsConfig[nwName]
658
-		if ep.MacAddress == "" {
659
-			ep.MacAddress = deprecatedMacAddress
660
-		} else if ep.MacAddress != deprecatedMacAddress {
661
-			return "", errdefs.InvalidParameter(errors.New("the container-wide MAC address should match the endpoint-specific MAC address for the main network or should be left empty"))
669
+		// If there's no endpoint config, create a place to store the configured address.
670
+		if len(networkingConfig.EndpointsConfig) == 0 {
671
+			networkingConfig.EndpointsConfig[nwName] = &network.EndpointSettings{
672
+				MacAddress: deprecatedMacAddress,
673
+			}
674
+		} else {
675
+			// There is existing endpoint config - if it's not indexed by NetworkMode.Name(), we
676
+			// can't tell which network the container-wide settings was intended for. NetworkMode,
677
+			// the keys in EndpointsConfig and the NetworkID in EndpointsConfig may mix network
678
+			// name/id/short-id. It's not safe to create EndpointsConfig under the NetworkMode
679
+			// name to store the container-wide MAC address, because that may result in two sets
680
+			// of EndpointsConfig for the same network and one set will be discarded later. So,
681
+			// reject the request ...
682
+			ep, ok := networkingConfig.EndpointsConfig[nwName]
683
+			if !ok {
684
+				return "", errdefs.InvalidParameter(errors.New("if a container-wide MAC address is supplied, HostConfig.NetworkMode must match the identity of a network in NetworkSettings.Networks"))
685
+			}
686
+			// ep is the endpoint that needs the container-wide MAC address; migrate the address
687
+			// to it, or bail out if there's a mismatch.
688
+			if ep.MacAddress == "" {
689
+				ep.MacAddress = deprecatedMacAddress
690
+			} else if ep.MacAddress != deprecatedMacAddress {
691
+				return "", errdefs.InvalidParameter(errors.New("the container-wide MAC address must match the endpoint-specific MAC address for the main network, or be left empty"))
692
+			}
662 693
 		}
663 694
 	}
664 695
 	warning = "The container-wide MacAddress field is now deprecated. It should be specified in EndpointsConfig instead."
665 696
new file mode 100644
... ...
@@ -0,0 +1,160 @@
0
+package container
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/docker/docker/api/types/container"
6
+	"github.com/docker/docker/api/types/network"
7
+	"gotest.tools/v3/assert"
8
+	is "gotest.tools/v3/assert/cmp"
9
+)
10
+
11
+func TestHandleMACAddressBC(t *testing.T) {
12
+	testcases := []struct {
13
+		name                string
14
+		apiVersion          string
15
+		ctrWideMAC          string
16
+		networkMode         container.NetworkMode
17
+		epConfig            map[string]*network.EndpointSettings
18
+		expEpWithCtrWideMAC string
19
+		expEpWithNoMAC      string
20
+		expCtrWideMAC       string
21
+		expWarning          string
22
+		expError            string
23
+	}{
24
+		{
25
+			name:                "old api ctr-wide mac mix id and name",
26
+			apiVersion:          "1.43",
27
+			ctrWideMAC:          "11:22:33:44:55:66",
28
+			networkMode:         "aNetId",
29
+			epConfig:            map[string]*network.EndpointSettings{"aNetName": {}},
30
+			expEpWithCtrWideMAC: "aNetName",
31
+			expCtrWideMAC:       "11:22:33:44:55:66",
32
+		},
33
+		{
34
+			name:           "old api clear ep mac",
35
+			apiVersion:     "1.43",
36
+			networkMode:    "aNetId",
37
+			epConfig:       map[string]*network.EndpointSettings{"aNetName": {MacAddress: "11:22:33:44:55:66"}},
38
+			expEpWithNoMAC: "aNetName",
39
+		},
40
+		{
41
+			name:          "old api no-network ctr-wide mac",
42
+			apiVersion:    "1.43",
43
+			networkMode:   "none",
44
+			ctrWideMAC:    "11:22:33:44:55:66",
45
+			expError:      "conflicting options: mac-address and the network mode",
46
+			expCtrWideMAC: "11:22:33:44:55:66",
47
+		},
48
+		{
49
+			name:                "old api create ep",
50
+			apiVersion:          "1.43",
51
+			networkMode:         "aNetId",
52
+			ctrWideMAC:          "11:22:33:44:55:66",
53
+			epConfig:            map[string]*network.EndpointSettings{},
54
+			expEpWithCtrWideMAC: "aNetId",
55
+			expCtrWideMAC:       "11:22:33:44:55:66",
56
+		},
57
+		{
58
+			name:                "old api migrate ctr-wide mac",
59
+			apiVersion:          "1.43",
60
+			ctrWideMAC:          "11:22:33:44:55:66",
61
+			networkMode:         "aNetName",
62
+			epConfig:            map[string]*network.EndpointSettings{"aNetName": {}},
63
+			expEpWithCtrWideMAC: "aNetName",
64
+			expCtrWideMAC:       "11:22:33:44:55:66",
65
+		},
66
+		{
67
+			name:        "new api no macs",
68
+			apiVersion:  "1.44",
69
+			networkMode: "aNetId",
70
+			epConfig:    map[string]*network.EndpointSettings{"aNetName": {}},
71
+		},
72
+		{
73
+			name:        "new api ep specific mac",
74
+			apiVersion:  "1.44",
75
+			networkMode: "aNetName",
76
+			epConfig:    map[string]*network.EndpointSettings{"aNetName": {MacAddress: "11:22:33:44:55:66"}},
77
+		},
78
+		{
79
+			name:                "new api migrate ctr-wide mac to new ep",
80
+			apiVersion:          "1.44",
81
+			ctrWideMAC:          "11:22:33:44:55:66",
82
+			networkMode:         "aNetName",
83
+			epConfig:            map[string]*network.EndpointSettings{},
84
+			expEpWithCtrWideMAC: "aNetName",
85
+			expWarning:          "The container-wide MacAddress field is now deprecated",
86
+			expCtrWideMAC:       "",
87
+		},
88
+		{
89
+			name:                "new api migrate ctr-wide mac to existing ep",
90
+			apiVersion:          "1.44",
91
+			ctrWideMAC:          "11:22:33:44:55:66",
92
+			networkMode:         "aNetName",
93
+			epConfig:            map[string]*network.EndpointSettings{"aNetName": {}},
94
+			expEpWithCtrWideMAC: "aNetName",
95
+			expWarning:          "The container-wide MacAddress field is now deprecated",
96
+			expCtrWideMAC:       "",
97
+		},
98
+		{
99
+			name:          "new api mode vs name mismatch",
100
+			apiVersion:    "1.44",
101
+			ctrWideMAC:    "11:22:33:44:55:66",
102
+			networkMode:   "aNetId",
103
+			epConfig:      map[string]*network.EndpointSettings{"aNetName": {}},
104
+			expError:      "if a container-wide MAC address is supplied, HostConfig.NetworkMode must match the identity of a network in NetworkSettings.Networks",
105
+			expCtrWideMAC: "11:22:33:44:55:66",
106
+		},
107
+		{
108
+			name:          "new api mac mismatch",
109
+			apiVersion:    "1.44",
110
+			ctrWideMAC:    "11:22:33:44:55:66",
111
+			networkMode:   "aNetName",
112
+			epConfig:      map[string]*network.EndpointSettings{"aNetName": {MacAddress: "00:11:22:33:44:55"}},
113
+			expError:      "the container-wide MAC address must match the endpoint-specific MAC address",
114
+			expCtrWideMAC: "11:22:33:44:55:66",
115
+		},
116
+	}
117
+
118
+	for _, tc := range testcases {
119
+		t.Run(tc.name, func(t *testing.T) {
120
+			cfg := &container.Config{
121
+				MacAddress: tc.ctrWideMAC, //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
122
+			}
123
+			hostCfg := &container.HostConfig{
124
+				NetworkMode: tc.networkMode,
125
+			}
126
+			epConfig := make(map[string]*network.EndpointSettings, len(tc.epConfig))
127
+			for k, v := range tc.epConfig {
128
+				v := v
129
+				epConfig[k] = v
130
+			}
131
+			netCfg := &network.NetworkingConfig{
132
+				EndpointsConfig: epConfig,
133
+			}
134
+
135
+			warning, err := handleMACAddressBC(cfg, hostCfg, netCfg, tc.apiVersion)
136
+
137
+			if tc.expError == "" {
138
+				assert.Check(t, err)
139
+			} else {
140
+				assert.Check(t, is.ErrorContains(err, tc.expError))
141
+			}
142
+			if tc.expWarning == "" {
143
+				assert.Check(t, is.Equal(warning, ""))
144
+			} else {
145
+				assert.Check(t, is.Contains(warning, tc.expWarning))
146
+			}
147
+			if tc.expEpWithCtrWideMAC != "" {
148
+				got := netCfg.EndpointsConfig[tc.expEpWithCtrWideMAC].MacAddress
149
+				assert.Check(t, is.Equal(got, tc.ctrWideMAC))
150
+			}
151
+			if tc.expEpWithNoMAC != "" {
152
+				got := netCfg.EndpointsConfig[tc.expEpWithNoMAC].MacAddress
153
+				assert.Check(t, is.Equal(got, ""))
154
+			}
155
+			gotCtrWideMAC := cfg.MacAddress //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
156
+			assert.Check(t, is.Equal(gotCtrWideMAC, tc.expCtrWideMAC))
157
+		})
158
+	}
159
+}
... ...
@@ -231,3 +231,52 @@ func TestInspectCfgdMAC(t *testing.T) {
231 231
 		})
232 232
 	}
233 233
 }
234
+
235
+// Regression test for https://github.com/moby/moby/issues/47441
236
+// Migration of a container-wide MAC address to the new per-endpoint setting,
237
+// where NetworkMode uses network id, and the key in endpoint settings is the
238
+// network name.
239
+func TestWatchtowerCreate(t *testing.T) {
240
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "no macvlan")
241
+
242
+	ctx := setupTest(t)
243
+
244
+	d := daemon.New(t)
245
+	d.StartWithBusybox(ctx, t)
246
+	defer d.Stop(t)
247
+
248
+	c := d.NewClientT(t, client.WithVersion("1.25"))
249
+	defer c.Close()
250
+
251
+	// Create a "/29" network, with a single address in iprange for IPAM to
252
+	// allocate, but no gateway address. So, the gateway will get the single
253
+	// free address. It'll only be possible to start a container by explicitly
254
+	// assigning an address.
255
+	const netName = "wtmvl"
256
+	netId := network.CreateNoError(ctx, t, c, netName,
257
+		network.WithIPAMRange("172.30.0.0/29", "172.30.0.1/32", ""),
258
+		network.WithDriver("macvlan"),
259
+	)
260
+	defer network.RemoveNoError(ctx, t, c, netName)
261
+
262
+	// Start a container, using the network's id in NetworkMode but its name
263
+	// in EndpointsConfig. (The container-wide MAC address must be merged with
264
+	// the endpoint config containing the preferred IP address, but the names
265
+	// don't match.)
266
+	const ctrName = "ctr1"
267
+	const ctrIP = "172.30.0.2"
268
+	const ctrMAC = "02:42:ac:11:00:42"
269
+	id := container.Run(ctx, t, c,
270
+		container.WithName(ctrName),
271
+		container.WithNetworkMode(netId),
272
+		container.WithContainerWideMacAddress(ctrMAC),
273
+		container.WithIPv4(netName, ctrIP),
274
+	)
275
+	defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
276
+
277
+	// Check that the container got the expected addresses.
278
+	inspect := container.Inspect(ctx, t, c, ctrName)
279
+	netSettings := inspect.NetworkSettings.Networks[netName]
280
+	assert.Check(t, is.Equal(netSettings.IPAddress, ctrIP))
281
+	assert.Check(t, is.Equal(netSettings.MacAddress, ctrMAC))
282
+}