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:
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")
+}