Repository: cdk8s-team/cdk8s-plus (ts)

Issue:
Support multi-protocol ports on containers
<!--Please make sure to read the "Reporting Issues" section of the contribution guide before submitting an issue
https://github.com/cdk8s-team/cdk8s/blob/master/CONTRIBUTING.md#reporting-issues
-->

### Description of the bug:
There are some situations where the same port on a container can serve multiple protocols. One example being DNS servers typically serving both TCP and UDP traffic from port 53. The `Container` construct, however, fails when a port number repeats, even if the specified protocol is different.

There are examples of specifying the same port multiple times with different protocols. One below is an example of a `docker-compose.yml` file where this happens:
```
version: "2.1"
services:
  blocky:
    image: spx01/blocky
    container_name: blocky
    restart: unless-stopped
    # Optional the instance hostname for logging purpose
    hostname: blocky-hostname
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "4000:4000/tcp"
    environment:
      - TZ=Europe/Berlin # Optional to synchronize the log timestamp with host
    volumes:
      # Optional to synchronize the log timestamp with host
      - /etc/localtime:/etc/localtime:ro
      # config file
      - ./config.yml:/app/config.yml
```

### Reproduction Steps:
Synth the following `Deployment` in any context
```
new Deployment(app, 'test-depl', {
    containers: [{
        name: 'blocky',
        ports: [{
                name: 'dns-tcp',
                number: 53,
                protocol: Protocol.TCP,
            }, {
                name: 'dns-udp',
                number: 53,
                protocol: Protocol.UDP,
            }, {
                name: "http",
                number: 4000,
                protocol: Protocol.TCP,
            }],
    }],
});
```


### Error Log:
As of `cdk8s-plus-27@2.7.70` trying to specify the same port number twice results in the following error:
```
Error: Port with number 53 already exists
    at Container.addPort (/Users/wyvernzora/Code/k2/gitops/infra/dns/node_modules/cdk8s-plus-27/src/container.ts:830:13)
    at new Container (/Users/wyvernzora/Code/k2/gitops/infra/dns/node_modules/cdk8s-plus-27/src/container.ts:770:12)
   ...
```

### Environment:


  - **Framework Version**: 
      - `cdk8s-cli@2.198.21`
      - `cdk8s@2.68.22`
      - `cdk8s-plus-27@2.7.70`
  - **OS**: MacOS Sonoma 14.2


### Other:
The culprit looks like [this comparison here](https://github.com/cdk8s-team/cdk8s-plus/blob/a1b57f55959771e9490d893545771dde9339df88/src/container.ts#L829). I think the protocol should also be factored in here. Or, alternatively, allow specifying multiple protocols per ContainerPort.

---

This is :bug: Bug Report

Pull request description:
fix(container): allow the same port number with different protocols
Fixes #3507 

Patch:
diff --git a/src/container.ts b/src/container.ts
index 3357c648..28048a6e 100644
--- a/src/container.ts
+++ b/src/container.ts
@@ -820,14 +820,15 @@ export class Container {
   public addPort(port: ContainerPort) {
 
     const names = this._ports.map(p => p.name).filter(x => x);
-    const numbers = this._ports.map(p => p.number);
+    const numberProtocols = this._ports.map(p => `${p.number}/${p.protocol || Protocol.TCP}`);
 
     if (port.name && names.includes(port.name)) {
       throw new Error(`Port with name ${port.name} already exists`);
     }
 
-    if (numbers.includes(port.number)) {
-      throw new Error(`Port with number ${port.number} already exists`);
+    const protocol = `${port.number}/${port.protocol || Protocol.TCP}`;
+    if (numberProtocols.includes(protocol)) {
+      throw new Error(`Port with number ${port.number} and protocol ${port.protocol || Protocol.TCP} already exists`);
     }
 
     this._ports.push(port);


Test patch:
diff --git a/test/container.test.ts b/test/container.test.ts
index 331b6fd5..eccb9ba7 100644
--- a/test/container.test.ts
+++ b/test/container.test.ts
@@ -180,23 +180,56 @@ describe('EnvValue', () => {
 
 describe('Container', () => {
 
-  test('cannot configure identical ports at instantiation', () => {
+  test('cannot configure identical ports and protocols at instantiation', () => {
 
     expect(() => new kplus.Container({
       image: 'image',
       ports: [
         {
           number: 8080,
+          protocol: kplus.Protocol.TCP,
         },
         {
           number: 8080,
+          protocol: kplus.Protocol.TCP,
         },
       ],
-    })).toThrowError('Port with number 8080 already exists');
+    })).toThrowError('Port with number 8080 and protocol TCP already exists');
 
   });
 
-  test('cannot add an already existing port number', () => {
+  test('can configure identical ports with different protocols at instantiation', () => {
+    const container = new kplus.Container({
+      image: 'image',
+      ports: [
+        {
+          number: 8080,
+          protocol: kplus.Protocol.TCP,
+        },
+        {
+          number: 8080,
+          protocol: kplus.Protocol.UDP,
+        },
+      ],
+    });
+
+    expect(container._toKube().ports).toEqual([{
+      containerPort: 8080,
+      protocol: 'TCP',
+    }, {
+      containerPort: 8080,
+      protocol: 'UDP',
+    }]);
+    expect(container.ports).toEqual([{
+      number: 8080,
+      protocol: kplus.Protocol.TCP,
+    }, {
+      number: 8080,
+      protocol: kplus.Protocol.UDP,
+    }]);
+  });
+
+  test('cannot add an already existing port number with identical protocol', () => {
 
     const container = new kplus.Container({
       image: 'image',
@@ -205,8 +238,38 @@ describe('Container', () => {
       }],
     });
 
-    expect(() => container.addPort({ number: 8080 })).toThrowError('Port with number 8080 already exists');
+    expect(() => container.addPort({ number: 8080 })).toThrowError('Port with number 8080 and protocol TCP already exists');
+
+  });
+
+  test('can add an already existing port number with a different protocol', () => {
+
+    const container = new kplus.Container({
+      image: 'image',
+      ports: [{
+        number: 8080,
+        protocol: kplus.Protocol.TCP,
+      }],
+    });
+    container.addPort({
+      number: 8080,
+      protocol: kplus.Protocol.UDP,
+    });
 
+    expect(container._toKube().ports).toEqual([{
+      containerPort: 8080,
+      protocol: 'TCP',
+    }, {
+      containerPort: 8080,
+      protocol: 'UDP',
+    }]);
+    expect(container.ports).toEqual([{
+      number: 8080,
+      protocol: kplus.Protocol.TCP,
+    }, {
+      number: 8080,
+      protocol: kplus.Protocol.UDP,
+    }]);
   });
 
   test('cannot add an already existing port name', () => {