api: Add a field MacAddress to EndpointSettings
Fix:

- fixes https://github.com/moby/moby/issues/34720

Related to:

- https://github.com/moby/moby/issues/31964
- https://github.com/docker/cli/pull/4419

**- What I did**

* Add a new `MacAddress` field to `EndpointSettings` ;
* Deprecate the old `MacAddress` in container's `Config` ;

**- Description for the changelog**

A MAC address can now be specified per-endpoint when creating a container.

**- A picture of a cute animal (not mandatory but encouraged)**

![](https://www.rd.com/wp-content/uploads/2020/05/GettyImages-83520439-e1588351839186.jpg)

api/types/network/endpoint.go
package network

import (
	"errors"
	"fmt"
	"net"

	"github.com/docker/docker/internal/multierror"
)

// EndpointSettings stores the network endpoint details
type EndpointSettings struct {
	// Configurations
	IPAMConfig *EndpointIPAMConfig
	Links      []string
	Aliases    []string
	// Operational data
	NetworkID           string
	EndpointID          string
	Gateway             string
	IPAddress           string
	IPPrefixLen         int
	IPv6Gateway         string
	GlobalIPv6Address   string
	GlobalIPv6PrefixLen int
	MacAddress          string
	DriverOpts          map[string]string
}

// Copy makes a deep copy of `EndpointSettings`
func (es *EndpointSettings) Copy() *EndpointSettings {
	epCopy := *es
	if es.IPAMConfig != nil {
		epCopy.IPAMConfig = es.IPAMConfig.Copy()
	}

	if es.Links != nil {
		links := make([]string, 0, len(es.Links))
		epCopy.Links = append(links, es.Links...)
	}

	if es.Aliases != nil {
		aliases := make([]string, 0, len(es.Aliases))
		epCopy.Aliases = append(aliases, es.Aliases...)
	}
	return &epCopy
}

// EndpointIPAMConfig represents IPAM configurations for the endpoint
type EndpointIPAMConfig struct {
	IPv4Address  string   `json:",omitempty"`
	IPv6Address  string   `json:",omitempty"`
	LinkLocalIPs []string `json:",omitempty"`
}

// Copy makes a copy of the endpoint ipam config
func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
	cfgCopy := *cfg
	cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs))
	cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...)
	return &cfgCopy
}

// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an
// EndpointIPAMConfig is valid for a specific network.
type NetworkSubnet interface {
	// Contains checks whether the NetworkSubnet contains [addr].
	Contains(addr net.IP) bool
	// IsStatic checks whether the subnet was statically allocated (ie. user-defined).
	IsStatic() bool
}

// IsInRange checks whether static IP addresses are valid in a specific network.
func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error {
	var errs []error

	if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
		errs = append(errs, err)
	}
	if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
		errs = append(errs, err)
	}

	return multierror.Join(errs...)
}

func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error {
	if epAddr == "" {
		return nil
	}

	var staticSubnet bool
	parsedAddr := net.ParseIP(epAddr)
	for _, subnet := range ipamSubnets {
		if subnet.IsStatic() {
			staticSubnet = true
			if subnet.Contains(parsedAddr) {
				return nil
			}
		}
	}

	if staticSubnet {
		return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
	}

	return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
}

// Validate checks whether cfg is valid.
func (cfg *EndpointIPAMConfig) Validate() error {
	if cfg == nil {
		return nil
	}

	var errs []error

	if cfg.IPv4Address != "" {
		if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
			errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address))
		}
	}
	if cfg.IPv6Address != "" {
		if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
			errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address))
		}
	}
	for _, addr := range cfg.LinkLocalIPs {
		if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() {
			errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr))
		}
	}

	return multierror.Join(errs...)
}

package network

import (
	"errors"
	"fmt"
	"net"

	"github.com/docker/docker/internal/multierror"
)

// EndpointSettings stores the network endpoint details
type EndpointSettings struct {
	// Configurations
	IPAMConfig *EndpointIPAMConfig
	Links      []string
	Aliases    []string
	MacAddress string
	// Operational data
	NetworkID           string
	EndpointID          string
	Gateway             string
	IPAddress           string
	IPPrefixLen         int
	IPv6Gateway         string
	GlobalIPv6Address   string
	GlobalIPv6PrefixLen int
	DriverOpts          map[string]string
}

// Copy makes a deep copy of `EndpointSettings`
func (es *EndpointSettings) Copy() *EndpointSettings {
	epCopy := *es
	if es.IPAMConfig != nil {
		epCopy.IPAMConfig = es.IPAMConfig.Copy()
	}

	if es.Links != nil {
		links := make([]string, 0, len(es.Links))
		epCopy.Links = append(links, es.Links...)
	}

	if es.Aliases != nil {
		aliases := make([]string, 0, len(es.Aliases))
		epCopy.Aliases = append(aliases, es.Aliases...)
	}
	return &epCopy
}

// EndpointIPAMConfig represents IPAM configurations for the endpoint
type EndpointIPAMConfig struct {
	IPv4Address  string   `json:",omitempty"`
	IPv6Address  string   `json:",omitempty"`
	LinkLocalIPs []string `json:",omitempty"`
}

// Copy makes a copy of the endpoint ipam config
func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
	cfgCopy := *cfg
	cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs))
	cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...)
	return &cfgCopy
}

// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an
// EndpointIPAMConfig is valid for a specific network.
type NetworkSubnet interface {
	// Contains checks whether the NetworkSubnet contains [addr].
	Contains(addr net.IP) bool
	// IsStatic checks whether the subnet was statically allocated (ie. user-defined).
	IsStatic() bool
}

// IsInRange checks whether static IP addresses are valid in a specific network.
func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error {
	var errs []error

	if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
		errs = append(errs, err)
	}
	if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
		errs = append(errs, err)
	}

	return multierror.Join(errs...)
}

func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error {
	if epAddr == "" {
		return nil
	}

	var staticSubnet bool
	parsedAddr := net.ParseIP(epAddr)
	for _, subnet := range ipamSubnets {
		if subnet.IsStatic() {
			staticSubnet = true
			if subnet.Contains(parsedAddr) {
				return nil
			}
		}
	}

	if staticSubnet {
		return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
	}

	return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
}

// Validate checks whether cfg is valid.
func (cfg *EndpointIPAMConfig) Validate() error {
	if cfg == nil {
		return nil
	}

	var errs []error

	if cfg.IPv4Address != "" {
		if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
			errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address))
		}
	}
	if cfg.IPv6Address != "" {
		if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
			errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address))
		}
	}
	for _, addr := range cfg.LinkLocalIPs {
		if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() {
			errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr))
		}
	}

	return multierror.Join(errs...)
}

I think (within reason) Go compatibility is ok to break, or at least, I'm comfortable with breaking if it causes a compile-failure (so no risk at things going unnoticed). Breaking the API is something to be more careful about.

I know we discussed my draft PR for splitting the operational data; https://github.com/moby/moby/pull/46059, and that other maintainers were in favor of that change, but ISTR there was a suggestion to match "k8s" and "swarmkit" terminology (use "spec" for desired data).

Wondering now if that PR would allow us to remove the ambiguity; would _could_ mean 2 separate fields for MacAddress.