Repository: docker/compose (go)

Issue:
Using mac_address with multiple networks is unreliable in Compose V2
**Description**

In Compose V1 if a service is attached to multiple networks and `mac_address` is set the chosen MAC is applied to the interface of the network with the highest priority. Network priority is determined based on the `priority` property (if set), and otherwise falls back to being based on the alphabetical order of network names (highest first).
In Compose V2 the chosen MAC address seems to be randomly assigned to either network.

**Steps to reproduce the issue:**
1. Use this compose file

```yaml
services:
  web:
    image: alpine
    command: ip l
    networks:
      back:
        priority: 100
      front:
        priority: 1000
    mac_address: 00:bb:bb:bb:bb:bb
networks:
  front:
    driver_opts:
      com.docker.network.container_iface_prefix: front
  back:
    driver_opts:
      com.docker.network.container_iface_prefix: back
```

2. Run `docker-compose-v1 up`
```
Attaching to bug-compose-mac_web_1
[...]
web_1  | 216: front0@if217: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
web_1  |     link/ether 00:bb:bb:bb:bb:bb brd ff:ff:ff:ff:ff:ff
web_1  | 218: back0@if219: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
web_1  |     link/ether 02:42:ac:15:00:02 brd ff:ff:ff:ff:ff:ff
bug-compose-mac_web_1 exited with code 0
```

3. Run `docker compose up`

```
[+] Running 1/1
 ⠿ Container bug-compose-mac_web_1  Recreated                                                                                                                                                                 0.1s
Attaching to bug-compose-mac-web-1
[...]
bug-compose-mac-web-1  | 220: front0@if221: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
bug-compose-mac-web-1  |     link/ether 02:42:ac:14:00:02 brd ff:ff:ff:ff:ff:ff
bug-compose-mac-web-1  | 222: back0@if223: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
bug-compose-mac-web-1  |     link/ether 00:bb:bb:bb:bb:bb brd ff:ff:ff:ff:ff:ff
bug-compose-mac-web-1 exited with code 0
```
**Describe the results you received:**

With compose v1, `00:bb:bb:bb:bb:bb` is affected to `front0`.
With compose v2, `00:bb:bb:bb:bb:bb` is affected to `back0`.

**Describe the results you expected:**

I expected `00:bb:bb:bb:bb:bb` to always be affected to `front0` in v2 because `front` has higher priority, just like in v1.

**Additional information you deem important (e.g. issue happens only occasionally):**

In V2 it seems like the network to which `mac_address` is assigned to is random and unpredictable, or if there's any logic I couldn't understand it. I've had occasions where even a totally minute modification like changing priorities to 100/100 instead of 10/100 or changing the service command and that alone flipped the attribution of the MAC between front0 and back0.

**Output of `docker compose version`:**

```
Docker Compose version v2.2.1
```

**Output of `docker info`:**

```
Client:
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc., v0.7.1)
  compose: Docker Compose (Docker Inc., v2.2.1)
  scan: Docker Scan (Docker Inc., v0.14.0)

Server:
 Containers: 1
  Running: 0
  Paused: 0
  Stopped: 1
 Images: 10
 Server Version: 20.10.11
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc version: v1.0.2-0-g52b36a2
 init version: de40ad0
 Security Options:
  seccomp
   Profile: default
  cgroupns
 Kernel Version: 5.10.76-linuxkit
 Operating System: Docker Desktop
 OSType: linux
 Architecture: x86_64
 CPUs: 2
 Total Memory: 1.94GiB
 Name: docker-desktop
 ID: W762:H2DL:BGE6:C4RJ:JM5Z:DBN5:IOQS:CHPW:AJQA:WOVU:E2QN:GPAM
 Docker Root Dir: /var/lib/docker
 Debug Mode: true
  File Descriptors: 44
  Goroutines: 46
  System Time: 2022-01-03T22:32:18.122512425Z
  EventsListeners: 3
 HTTP Proxy: http.docker.internal:3128
 HTTPS Proxy: http.docker.internal:3128
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: true
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
```

Pull request:
Set NetworkMode correctly according to network priorities
Signed-off-by: Laura Brehm <laurabrehm@hey.com>

**What I did**
Sets NetworkMode according to network priority on container create call

**Related issue**
Fixes #9076 


Patch:
diff --git a/pkg/compose/create.go b/pkg/compose/create.go
index 1bd57cf0e..ebedcb3f7 100644
--- a/pkg/compose/create.go
+++ b/pkg/compose/create.go
@@ -445,14 +445,16 @@ func (s *composeService) prepareLabels(p *types.Project, service types.ServiceCo
 }
 
 func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string {
-	mode := "none"
-	if len(project.Networks) > 0 {
-		for name := range getNetworksForService(service) {
-			mode = project.Networks[name].Name
-			break
-		}
+	if len(project.Networks) == 0 {
+		return "none"
 	}
-	return mode
+
+	if len(service.Networks) > 0 {
+		name := service.NetworksByPriority()[0]
+		return project.Networks[name].Name
+	}
+
+	return project.Networks["default"].Name
 }
 
 func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
@@ -1013,16 +1015,6 @@ func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
 	return aliases
 }
 
-func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
-	if len(s.Networks) > 0 {
-		return s.Networks
-	}
-	if s.NetworkMode != "" {
-		return nil
-	}
-	return map[string]*types.ServiceNetworkConfig{"default": nil}
-}
-
 func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
 	_, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
 	if err != nil {


Test patch:
diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go
index 254e0b004..ea2ec88ad 100644
--- a/pkg/compose/create_test.go
+++ b/pkg/compose/create_test.go
@@ -151,3 +151,74 @@ func TestGetBindMode(t *testing.T) {
 	assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxShared}, true), "ro,z")
 	assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxPrivate}, true), "ro,Z")
 }
+
+func TestGetDefaultNetworkMode(t *testing.T) {
+	t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) {
+		service := composetypes.ServiceConfig{
+			Name: "myService",
+			Networks: map[string]*composetypes.ServiceNetworkConfig{
+				"myNetwork1": {
+					Priority: 10,
+				},
+				"myNetwork2": {
+					Priority: 1000,
+				},
+			},
+		}
+		project := composetypes.Project{
+			Name: "myProject",
+			Services: []composetypes.ServiceConfig{
+				service,
+			},
+			Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
+				"myNetwork1": {
+					Name: "myProject_myNetwork1",
+				},
+				"myNetwork2": {
+					Name: "myProject_myNetwork2",
+				},
+			}),
+		}
+
+		assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_myNetwork2")
+	})
+
+	t.Run("returns default network when service has no networks", func(t *testing.T) {
+		service := composetypes.ServiceConfig{
+			Name: "myService",
+		}
+		project := composetypes.Project{
+			Name: "myProject",
+			Services: []composetypes.ServiceConfig{
+				service,
+			},
+			Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
+				"myNetwork1": {
+					Name: "myProject_myNetwork1",
+				},
+				"myNetwork2": {
+					Name: "myProject_myNetwork2",
+				},
+				"default": {
+					Name: "myProject_default",
+				},
+			}),
+		}
+
+		assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_default")
+	})
+
+	t.Run("returns none if project has no networks", func(t *testing.T) {
+		service := composetypes.ServiceConfig{
+			Name: "myService",
+		}
+		project := composetypes.Project{
+			Name: "myProject",
+			Services: []composetypes.ServiceConfig{
+				service,
+			},
+		}
+
+		assert.Equal(t, getDefaultNetworkMode(&project, service), "none")
+	})
+}