diff --git a/api/swagger.yaml b/api/swagger.yaml index 3c3d83fb9a..c5725d73c4 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1381,6 +1381,7 @@ definitions: LinkLocalIPs: - "169.254.34.68" - "fe80::3468" + MacAddress: "02:42:ac:12:05:02" Links: - "container_1" - "container_2" @@ -2455,6 +2456,11 @@ definitions: example: - "container_1" - "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: type: "array" items: @@ -2505,11 +2511,6 @@ definitions: type: "integer" format: "int64" example: 64 - MacAddress: - description: | - MAC address for the endpoint on this network. - type: "string" - example: "02:42:ac:11:00:04" DriverOpts: description: | DriverOpts is a mapping of driver options and values. These options @@ -10130,6 +10131,7 @@ paths: IPAMConfig: IPv4Address: "172.24.56.89" IPv6Address: "2001:db8::5689" + MacAddress: "02:42:ac:12:05:02" tags: ["Network"] /networks/{id}/disconnect: diff --git a/api/types/network/endpoint.go b/api/types/network/endpoint.go index a119dca909..d7d835f6d5 100644 --- a/api/types/network/endpoint.go +++ b/api/types/network/endpoint.go @@ -14,6 +14,7 @@ type EndpointSettings struct { IPAMConfig *EndpointIPAMConfig Links []string Aliases []string + MacAddress string // Operational data NetworkID string EndpointID string @@ -23,7 +24,6 @@ type EndpointSettings struct { IPv6Gateway string GlobalIPv6Address string GlobalIPv6PrefixLen int - MacAddress string DriverOpts map[string]string } diff --git a/client/container_create.go b/client/container_create.go index 7a95461c88..ce9a7a404d 100644 --- a/client/container_create.go +++ b/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 { 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 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) } + +// 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 +} diff --git a/daemon/container_operations.go b/daemon/container_operations.go index 652f347689..2c5e04888e 100644 --- a/daemon/container_operations.go +++ b/daemon/container_operations.go @@ -628,7 +628,6 @@ func cleanOperationalData(es *network.EndpointSettings) { es.IPv6Gateway = "" es.GlobalIPv6Address = "" es.GlobalIPv6PrefixLen = 0 - es.MacAddress = "" if es.IPAMOperational { es.IPAMConfig = nil } diff --git a/daemon/network.go b/daemon/network.go index 5e467d7ed4..a49ded0f8b 100644 --- a/daemon/network.go +++ b/daemon/network.go @@ -788,6 +788,7 @@ func (daemon *Daemon) clearAttachableNetworks() { // 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) { var createOptions []libnetwork.EndpointOption + var genericOptions = make(options.Generic) nwName := n.Name() defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName() @@ -825,6 +826,14 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e for k, v := range epConfig.DriverOpts { 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 { @@ -863,9 +872,8 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e if err != nil { 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.CreateOptionPortMapping(publishedPorts), libnetwork.CreateOptionExposedPorts(exposedPorts)) + createOptions = append(createOptions, + libnetwork.CreateOptionPortMapping(publishedPorts), + libnetwork.CreateOptionExposedPorts(exposedPorts), + libnetwork.EndpointOptionGeneric(genericOptions)) return createOptions, nil } diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 21d91b7568..6d60ba46d3 100644 --- a/docs/api/version-history.md +++ b/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` and `AppArmor` fields in the `ContainerSpec.Privileges` object. This allows 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 diff --git a/integration/container/create_test.go b/integration/container/create_test.go index f30f9cdf07..9473ac611f 100644 --- a/integration/container/create_test.go +++ b/integration/container/create_test.go @@ -1,10 +1,12 @@ package container // import "github.com/docker/docker/integration/container" import ( + "bufio" "context" "encoding/json" "fmt" "strconv" + "strings" "testing" "time" @@ -14,6 +16,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/errdefs" 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/testutil" 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: mtu 65536 qdisc noqueue qlen 1000\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + // 134: eth0@if135: 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") + } +} diff --git a/integration/internal/container/ops.go b/integration/internal/container/ops.go index 03a7365c6a..2e29d4562a 100644 --- a/integration/internal/container/ops.go +++ b/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 func WithIPv4(networkName, ip string) func(*TestContainerConfig) { return func(c *TestContainerConfig) {