Repository: docker/compose-cli (go)

Issue:
ACI: 'run' command fails to create UDP protocol binding for public IP address 
**Steps to reproduce the issue:**
1. Create ACI Docker context e.g. 'acictx'
2. `docker --context acictx run -d -p 12345/udp some-udp-based-network-service-image`

**Describe the results you received:**
No errors/warnings, but the public IP address created as part of the ACI container group is using TCP protocol

**Describe the results you expected:**
The public IP address created as part of the ACI container group should use UDP protocol

**Additional information you deem important (e.g. issue happens only occasionally):**
You can verify the protocol used by the IP address with Azure CLI (`az container show`). Deploying via `az container create` works as expected (UDP protocol binding is created and communication works).

Tried on Docker Desktop for Mac 2.5.2 (50156). Also tried on Ubuntu Linux with the same effect.


Pull request description:
ACI: Allow setting protocol when publishing ports
Signed-off-by: Guillaume Tardif <guillaume.tardif@gmail.com>

**What I did**
* Add protocol conversion when creating ACI container group for tcp/udp
* deploy and check runtime `docker inspect` also returns the correct port protocol.
* added an error for unknown protocols

**Related issue**
Fixes #985 

<!-- optional tests
You can add a / mention to run tests executed by default only on main branch : 
* `test-aci` to run ACI E2E tests
* `test-ecs` to run ECS E2E tests
* `test-windows` to run tests & E2E tests on windows
-->
/test-aci

**(not mandatory) A picture of a cute animal, if possible in relation with what you did**


Patch:
diff --git a/aci/convert/container.go b/aci/convert/container.go
index c0bb4cd6..ab168e9e 100644
--- a/aci/convert/container.go
+++ b/aci/convert/container.go
@@ -32,6 +32,7 @@ func ContainerToComposeProject(r containers.ContainerConfig) (types.Project, err
 		ports = append(ports, types.ServicePortConfig{
 			Target:    p.ContainerPort,
 			Published: p.HostPort,
+			Protocol:  p.Protocol,
 		})
 	}
 
diff --git a/aci/convert/ports.go b/aci/convert/ports.go
index 4bb3693b..df55b546 100644
--- a/aci/convert/ports.go
+++ b/aci/convert/ports.go
@@ -37,12 +37,25 @@ func convertPortsToAci(service serviceConfigAciHelper) ([]containerinstance.Cont
 			return nil, nil, nil, errors.New(msg)
 		}
 		portNumber := int32(portConfig.Target)
+		var groupProtocol containerinstance.ContainerGroupNetworkProtocol
+		var containerProtocol containerinstance.ContainerNetworkProtocol
+		switch portConfig.Protocol {
+		case "tcp", "":
+			groupProtocol = containerinstance.TCP
+			containerProtocol = containerinstance.ContainerNetworkProtocolTCP
+		case "udp":
+			groupProtocol = containerinstance.UDP
+			containerProtocol = containerinstance.ContainerNetworkProtocolUDP
+		default:
+			return nil, nil, nil, fmt.Errorf("unknown protocol %q in exposed port for service %q", portConfig.Protocol, service.Name)
+		}
 		containerPorts = append(containerPorts, containerinstance.ContainerPort{
-			Port: to.Int32Ptr(portNumber),
+			Port:     to.Int32Ptr(portNumber),
+			Protocol: containerProtocol,
 		})
 		groupPorts = append(groupPorts, containerinstance.Port{
 			Port:     to.Int32Ptr(portNumber),
-			Protocol: containerinstance.TCP,
+			Protocol: groupProtocol,
 		})
 	}
 	var dnsLabelName *string = nil


Test patch:
diff --git a/aci/convert/ports_test.go b/aci/convert/ports_test.go
index a5c1acc8..7452f711 100644
--- a/aci/convert/ports_test.go
+++ b/aci/convert/ports_test.go
@@ -173,3 +173,85 @@ func TestPortConvert(t *testing.T) {
 		})
 	}
 }
+
+func TestConvertTCPPortsToAci(t *testing.T) {
+	service := types.ServiceConfig{
+		Name: "myService",
+		Ports: []types.ServicePortConfig{
+			{
+				Protocol:  "",
+				Target:    80,
+				Published: 80,
+			},
+			{
+				Protocol:  "tcp",
+				Target:    90,
+				Published: 90,
+			},
+		},
+	}
+	containerPorts, groupPports, _, err := convertPortsToAci(serviceConfigAciHelper(service))
+	assert.NilError(t, err)
+	assert.DeepEqual(t, containerPorts, []containerinstance.ContainerPort{
+		{
+			Port:     to.Int32Ptr(80),
+			Protocol: containerinstance.ContainerNetworkProtocolTCP,
+		},
+		{
+			Port:     to.Int32Ptr(90),
+			Protocol: containerinstance.ContainerNetworkProtocolTCP,
+		},
+	})
+	assert.DeepEqual(t, groupPports, []containerinstance.Port{
+		{
+			Port:     to.Int32Ptr(80),
+			Protocol: containerinstance.TCP,
+		},
+		{
+			Port:     to.Int32Ptr(90),
+			Protocol: containerinstance.TCP,
+		},
+	})
+}
+
+func TestConvertUDPPortsToAci(t *testing.T) {
+	service := types.ServiceConfig{
+		Name: "myService",
+		Ports: []types.ServicePortConfig{
+			{
+				Protocol:  "udp",
+				Target:    80,
+				Published: 80,
+			},
+		},
+	}
+	containerPorts, groupPports, _, err := convertPortsToAci(serviceConfigAciHelper(service))
+	assert.NilError(t, err)
+	assert.DeepEqual(t, containerPorts, []containerinstance.ContainerPort{
+		{
+			Port:     to.Int32Ptr(80),
+			Protocol: containerinstance.ContainerNetworkProtocolUDP,
+		},
+	})
+	assert.DeepEqual(t, groupPports, []containerinstance.Port{
+		{
+			Port:     to.Int32Ptr(80),
+			Protocol: containerinstance.UDP,
+		},
+	})
+}
+
+func TestConvertErrorOnMappingPorts(t *testing.T) {
+	service := types.ServiceConfig{
+		Name: "myService",
+		Ports: []types.ServicePortConfig{
+			{
+				Protocol:  "",
+				Target:    80,
+				Published: 90,
+			},
+		},
+	}
+	_, _, _, err := convertPortsToAci(serviceConfigAciHelper(service))
+	assert.Error(t, err, "Port mapping is not supported with ACI, cannot map port 90 to 80 for container myService")
+}