From 785835b1a28a04e00f381bcc8d372ecb174bba43 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 20 Nov 2023 16:37:59 +0100 Subject: [PATCH] Add support for endpoint-specific MAC address Related to: - https://github.com/compose-spec/compose-spec/pull/435 - https://github.com/moby/moby/pull/45905 Since API v1.44, Moby supports a per-endpoint MAC address and returns a warning when the container-wide mac_address field is set. A corresponding field has been added to compose-spec and compose-go, so we need to leverage it to set the right API field. This commit is backward-compatible with compose files that still set the container-wide mac_address field, and older API versions that don't know about the endpoint-specific MAC address field. Signed-off-by: Albin Kerouanton --- pkg/compose/create.go | 70 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 9f2e477fc..1ac5a1167 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -35,6 +35,7 @@ import ( "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/strslice" + "github.com/docker/docker/api/types/versions" volume_api "github.com/docker/docker/api/types/volume" "github.com/docker/docker/errdefs" "github.com/docker/go-connections/nat" @@ -178,6 +179,18 @@ func (s *composeService) getCreateConfigs(ctx context.Context, proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil)) env := proxyConfig.OverrideBy(service.Environment) + var mainNwName string + var mainNw *types.ServiceNetworkConfig + if len(service.Networks) > 0 { + mainNwName = service.NetworksByPriority()[0] + mainNw = service.Networks[mainNwName] + } + + macAddress, err := s.prepareContainerMACAddress(ctx, service, mainNw, mainNwName) + if err != nil { + return createConfigs{}, err + } + healthcheck, err := s.ToMobyHealthCheck(ctx, service.HealthCheck) if err != nil { return createConfigs{}, err @@ -198,7 +211,7 @@ func (s *composeService) getCreateConfigs(ctx context.Context, WorkingDir: service.WorkingDir, Entrypoint: entrypoint, NetworkDisabled: service.NetworkMode == "disabled", - MacAddress: service.MacAddress, + MacAddress: macAddress, Labels: labels, StopSignal: service.StopSignal, Env: ToMobyEnv(env), @@ -290,6 +303,58 @@ func (s *composeService) getCreateConfigs(ctx context.Context, return cfgs, nil } +// prepareContainerMACAddress handles the service-level mac_address field and the newer mac_address field added to service +// network config. This newer field is only compatible with the Engine API v1.44 (and onwards), and this API version +// also deprecates the container-wide mac_address field. Thus, this method will validate service config and mutate the +// passed mainNw to provide backward-compatibility whenever possible. +// +// It returns the container-wide MAC address, but this value will be kept empty for newer API versions. +func (s *composeService) prepareContainerMACAddress(ctx context.Context, service types.ServiceConfig, mainNw *types.ServiceNetworkConfig, nwName string) (string, error) { + version, err := s.RuntimeVersion(ctx) + if err != nil { + return "", err + } + + // Engine API 1.44 added support for endpoint-specific MAC address and now returns a warning when a MAC address is + // set in container.Config. Thus, we have to jump through a number of hoops: + // + // 1. Top-level mac_address and main endpoint's MAC address should be the same ; + // 2. If supported by the API, top-level mac_address should be migrated to the main endpoint and container.Config + // should be kept empty ; + // 3. Otherwise, the endpoint mac_address should be set in container.Config and no other endpoint-specific + // mac_address can be specified. If that's the case, use top-level mac_address ; + // + // After that, if an endpoint mac_address is set, it's either user-defined or migrated by the code below, so + // there's no need to check for API version in defaultNetworkSettings. + macAddress := service.MacAddress + if macAddress != "" && mainNw != nil && mainNw.MacAddress != "" && mainNw.MacAddress != macAddress { + return "", fmt.Errorf("the service-level mac_address should have the same value as network %s", nwName) + } + if versions.GreaterThanOrEqualTo(version, "1.44") { + if mainNw != nil && mainNw.MacAddress == "" { + mainNw.MacAddress = macAddress + } + macAddress = "" + } else if len(service.Networks) > 0 { + var withMacAddress []string + for nwName, nw := range service.Networks { + if nw != nil && nw.MacAddress != "" { + withMacAddress = append(withMacAddress, nwName) + } + } + + if len(withMacAddress) > 1 { + return "", fmt.Errorf("a MAC address is specified for multiple networks (%s), but this feature requires Docker Engine 1.44 or later (currently: %s)", strings.Join(withMacAddress, ", "), version) + } + + if mainNw != nil { + macAddress = mainNw.MacAddress + } + } + + return macAddress, nil +} + func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, useNetworkAliases bool) []string { aliases := []string{getContainerName(project.Name, service, serviceIndex)} if useNetworkAliases { @@ -307,6 +372,7 @@ func createEndpointSettings(p *types.Project, service types.ServiceConfig, servi var ( ipv4Address string ipv6Address string + macAddress string ) if config != nil { ipv4Address = config.Ipv4Address @@ -316,6 +382,7 @@ func createEndpointSettings(p *types.Project, service types.ServiceConfig, servi IPv6Address: ipv6Address, LinkLocalIPs: config.LinkLocalIPs, } + macAddress = config.MacAddress } return &network.EndpointSettings{ Aliases: getAliases(p, service, serviceIndex, networkKey, useNetworkAliases), @@ -323,6 +390,7 @@ func createEndpointSettings(p *types.Project, service types.ServiceConfig, servi IPAddress: ipv4Address, IPv6Gateway: ipv6Address, IPAMConfig: ipam, + MacAddress: macAddress, } }