Browse Source

api: Add a field MacAddress to EndpointSettings

Prior to this commit, only container.Config had a MacAddress field and
it's used only for the first network the container connects to. It's a
relic of old times where custom networks were not supported.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Albin Kerouanton 2 years ago
parent
commit
052562f

+ 7 - 5
api/swagger.yaml

@@ -1381,6 +1381,7 @@ definitions:
             LinkLocalIPs:
             LinkLocalIPs:
               - "169.254.34.68"
               - "169.254.34.68"
               - "fe80::3468"
               - "fe80::3468"
+          MacAddress: "02:42:ac:12:05:02"
           Links:
           Links:
             - "container_1"
             - "container_1"
             - "container_2"
             - "container_2"
@@ -2455,6 +2456,11 @@ definitions:
         example:
         example:
           - "container_1"
           - "container_1"
           - "container_2"
           - "container_2"
+      MacAddress:
+        description: |
+          MAC address for the endpoint on this network. The network driver might ignore this parameter.
+        type: "string"
+        example: "02:42:ac:11:00:04"
       Aliases:
       Aliases:
         type: "array"
         type: "array"
         items:
         items:
@@ -2505,11 +2511,6 @@ definitions:
         type: "integer"
         type: "integer"
         format: "int64"
         format: "int64"
         example: 64
         example: 64
-      MacAddress:
-        description: |
-          MAC address for the endpoint on this network.
-        type: "string"
-        example: "02:42:ac:11:00:04"
       DriverOpts:
       DriverOpts:
         description: |
         description: |
           DriverOpts is a mapping of driver options and values. These options
           DriverOpts is a mapping of driver options and values. These options
@@ -10130,6 +10131,7 @@ paths:
                 IPAMConfig:
                 IPAMConfig:
                   IPv4Address: "172.24.56.89"
                   IPv4Address: "172.24.56.89"
                   IPv6Address: "2001:db8::5689"
                   IPv6Address: "2001:db8::5689"
+                MacAddress: "02:42:ac:12:05:02"
       tags: ["Network"]
       tags: ["Network"]
 
 
   /networks/{id}/disconnect:
   /networks/{id}/disconnect:

+ 1 - 1
api/types/network/endpoint.go

@@ -14,6 +14,7 @@ type EndpointSettings struct {
 	IPAMConfig *EndpointIPAMConfig
 	IPAMConfig *EndpointIPAMConfig
 	Links      []string
 	Links      []string
 	Aliases    []string
 	Aliases    []string
+	MacAddress string
 	// Operational data
 	// Operational data
 	NetworkID           string
 	NetworkID           string
 	EndpointID          string
 	EndpointID          string
@@ -23,7 +24,6 @@ type EndpointSettings struct {
 	IPv6Gateway         string
 	IPv6Gateway         string
 	GlobalIPv6Address   string
 	GlobalIPv6Address   string
 	GlobalIPv6PrefixLen int
 	GlobalIPv6PrefixLen int
-	MacAddress          string
 	DriverOpts          map[string]string
 	DriverOpts          map[string]string
 }
 }
 
 

+ 16 - 0
client/container_create.go

@@ -39,6 +39,9 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
 	if err := cli.NewVersionError(ctx, "1.44", "specify health-check start interval"); config != nil && config.Healthcheck != nil && config.Healthcheck.StartInterval != 0 && err != nil {
 	if err := cli.NewVersionError(ctx, "1.44", "specify health-check start interval"); config != nil && config.Healthcheck != nil && config.Healthcheck.StartInterval != 0 && err != nil {
 		return response, err
 		return response, err
 	}
 	}
+	if err := cli.NewVersionError("1.44", "specify mac-address per network"); hasEndpointSpecificMacAddress(networkingConfig) && err != nil {
+		return response, err
+	}
 
 
 	if hostConfig != nil {
 	if hostConfig != nil {
 		if versions.LessThan(cli.ClientVersion(), "1.25") {
 		if versions.LessThan(cli.ClientVersion(), "1.25") {
@@ -91,3 +94,16 @@ func formatPlatform(platform *ocispec.Platform) string {
 	}
 	}
 	return path.Join(platform.OS, platform.Architecture, platform.Variant)
 	return path.Join(platform.OS, platform.Architecture, platform.Variant)
 }
 }
+
+// hasEndpointSpecificMacAddress checks whether one of the endpoint in networkingConfig has a MacAddress defined.
+func hasEndpointSpecificMacAddress(networkingConfig *network.NetworkingConfig) bool {
+	if networkingConfig == nil {
+		return false
+	}
+	for _, endpoint := range networkingConfig.EndpointsConfig {
+		if endpoint.MacAddress != "" {
+			return true
+		}
+	}
+	return false
+}

+ 0 - 1
daemon/container_operations.go

@@ -628,7 +628,6 @@ func cleanOperationalData(es *network.EndpointSettings) {
 	es.IPv6Gateway = ""
 	es.IPv6Gateway = ""
 	es.GlobalIPv6Address = ""
 	es.GlobalIPv6Address = ""
 	es.GlobalIPv6PrefixLen = 0
 	es.GlobalIPv6PrefixLen = 0
-	es.MacAddress = ""
 	if es.IPAMOperational {
 	if es.IPAMOperational {
 		es.IPAMConfig = nil
 		es.IPAMConfig = nil
 	}
 	}

+ 15 - 4
daemon/network.go

@@ -788,6 +788,7 @@ func (daemon *Daemon) clearAttachableNetworks() {
 // buildCreateEndpointOptions builds endpoint options from a given network.
 // buildCreateEndpointOptions builds endpoint options from a given network.
 func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, epConfig *network.EndpointSettings, sb *libnetwork.Sandbox, daemonDNS []string) ([]libnetwork.EndpointOption, error) {
 func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, epConfig *network.EndpointSettings, sb *libnetwork.Sandbox, daemonDNS []string) ([]libnetwork.EndpointOption, error) {
 	var createOptions []libnetwork.EndpointOption
 	var createOptions []libnetwork.EndpointOption
+	var genericOptions = make(options.Generic)
 
 
 	nwName := n.Name()
 	nwName := n.Name()
 	defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName()
 	defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName()
@@ -825,6 +826,14 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e
 		for k, v := range epConfig.DriverOpts {
 		for k, v := range epConfig.DriverOpts {
 			createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
 			createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
 		}
 		}
+
+		if epConfig.MacAddress != "" {
+			mac, err := net.ParseMAC(epConfig.MacAddress)
+			if err != nil {
+				return nil, err
+			}
+			genericOptions[netlabel.MacAddress] = mac
+		}
 	}
 	}
 
 
 	if svcCfg := c.NetworkSettings.Service; svcCfg != nil {
 	if svcCfg := c.NetworkSettings.Service; svcCfg != nil {
@@ -863,9 +872,8 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
-			createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{
-				netlabel.MacAddress: mac,
-			}))
+
+			genericOptions[netlabel.MacAddress] = mac
 		}
 		}
 	}
 	}
 
 
@@ -940,7 +948,10 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e
 		createOptions = append(createOptions, libnetwork.CreateOptionDNS(daemonDNS))
 		createOptions = append(createOptions, libnetwork.CreateOptionDNS(daemonDNS))
 	}
 	}
 
 
-	createOptions = append(createOptions, libnetwork.CreateOptionPortMapping(publishedPorts), libnetwork.CreateOptionExposedPorts(exposedPorts))
+	createOptions = append(createOptions,
+		libnetwork.CreateOptionPortMapping(publishedPorts),
+		libnetwork.CreateOptionExposedPorts(exposedPorts),
+		libnetwork.EndpointOptionGeneric(genericOptions))
 
 
 	return createOptions, nil
 	return createOptions, nil
 }
 }

+ 2 - 0
docs/api/version-history.md

@@ -55,6 +55,8 @@ keywords: "API, Docker, rcli, REST, documentation"
 * `POST /services/create` and `POST /services/{id}/update` now accept `Seccomp`
 * `POST /services/create` and `POST /services/{id}/update` now accept `Seccomp`
   and `AppArmor` fields in the `ContainerSpec.Privileges` object. This allows
   and `AppArmor` fields in the `ContainerSpec.Privileges` object. This allows
   some configuration of Seccomp and AppArmor in Swarm services.
   some configuration of Seccomp and AppArmor in Swarm services.
+* A new endpoint-specific `MacAddress` field has been added to `NetworkSettings.EndpointSettings`
+  on `POST /containers/create`, and to `EndpointConfig` on `POST /networks/{id}/connect`.
 
 
 ## v1.43 API changes
 ## v1.43 API changes
 
 

+ 42 - 0
integration/container/create_test.go

@@ -1,10 +1,12 @@
 package container // import "github.com/docker/docker/integration/container"
 package container // import "github.com/docker/docker/integration/container"
 
 
 import (
 import (
+	"bufio"
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"strconv"
 	"strconv"
+	"strings"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -14,6 +16,7 @@ import (
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/errdefs"
 	ctr "github.com/docker/docker/integration/internal/container"
 	ctr "github.com/docker/docker/integration/internal/container"
+	net "github.com/docker/docker/integration/internal/network"
 	"github.com/docker/docker/oci"
 	"github.com/docker/docker/oci"
 	"github.com/docker/docker/testutil"
 	"github.com/docker/docker/testutil"
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -631,3 +634,42 @@ func TestCreateWithMultipleEndpointSettings(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
+
+func TestCreateWithCustomMACs(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
+	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44")
+
+	ctx := setupTest(t)
+	apiClient := testEnv.APIClient()
+
+	net.CreateNoError(ctx, t, apiClient, "testnet")
+
+	attachCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
+	defer cancel()
+	res := ctr.RunAttach(attachCtx, t, apiClient,
+		ctr.WithCmd("ip", "-o", "link", "show"),
+		ctr.WithNetworkMode("bridge"),
+		ctr.WithMacAddress("bridge", "02:32:1c:23:00:04"))
+
+	assert.Equal(t, res.ExitCode, 0)
+	assert.Equal(t, res.Stderr.String(), "")
+
+	scanner := bufio.NewScanner(res.Stdout)
+	for scanner.Scan() {
+		fields := strings.Fields(scanner.Text())
+		// The expected output is:
+		// 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000\    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
+		// 134: eth0@if135: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1400 qdisc noqueue \    link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
+		if len(fields) < 11 {
+			continue
+		}
+
+		ifaceName := fields[1]
+		if ifaceName[:3] != "eth" {
+			continue
+		}
+
+		mac := fields[len(fields)-3]
+		assert.Equal(t, mac, "02:32:1c:23:00:04")
+	}
+}

+ 12 - 0
integration/internal/container/ops.go

@@ -114,6 +114,18 @@ func WithTmpfs(targetAndOpts string) func(config *TestContainerConfig) {
 	}
 	}
 }
 }
 
 
+func WithMacAddress(networkName, mac string) func(config *TestContainerConfig) {
+	return func(c *TestContainerConfig) {
+		if c.NetworkingConfig.EndpointsConfig == nil {
+			c.NetworkingConfig.EndpointsConfig = map[string]*network.EndpointSettings{}
+		}
+		if v, ok := c.NetworkingConfig.EndpointsConfig[networkName]; !ok || v == nil {
+			c.NetworkingConfig.EndpointsConfig[networkName] = &network.EndpointSettings{}
+		}
+		c.NetworkingConfig.EndpointsConfig[networkName].MacAddress = mac
+	}
+}
+
 // WithIPv4 sets the specified ip for the specified network of the container
 // WithIPv4 sets the specified ip for the specified network of the container
 func WithIPv4(networkName, ip string) func(*TestContainerConfig) {
 func WithIPv4(networkName, ip string) func(*TestContainerConfig) {
 	return func(c *TestContainerConfig) {
 	return func(c *TestContainerConfig) {