Repository: kubernetes/kubernetes (language: go)

Issue:
Named ports specified in sidecar container pod spec are not available to services
### What happened?

When migrating a sidecar container to a native sidecar container (initContainer with restartPolicy=Always), traffic going via the Kubernetes Service did not reach the application container/sidecar container.

### What did you expect to happen?

The traffic should have reached the sidecar container like before the migration.

### How can we reproduce it (as minimally and precisely as possible)?

Create a Pod with a native sidecar container (initContainer) with a named port, as well as a Kubernetes Service using that named port as its targetPort. This port is not listed in the Endpoint.

Apply the following:

```
apiVersion: v1
kind: Pod
metadata:
  name: sidecar-ports
  labels:
    app: sidecar-ports
spec:
  containers:
  - image: bloomberg/goldpinger:3.10.1
    name: goldpinger
    ports:
    - containerPort: 8000
      name: http
      protocol: TCP
  initContainers:
  # note: no envoy configuration is provided, the steps to reproduce involve reading other Kubernetes manifests (Endpoints)
  - image: envoyproxy/envoy:v1.73.7
    name: tls-terminator
    ports:
    - containerPort: 9443
      name: https
      protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: sidecar-ports
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: http
  - name: https
    port: 443
    protocol: TCP
    targetPort: https
  selector:
    app: sidecar-ports
  type: ClusterIP
```

The Endpoint backing the created Service "sidecar-ports" should contain two ports; "http" and "https", but it only contains "http":
```
k describe endpoints sidecar-ports
Name:         sidecar-ports
Namespace:    default
Subsets:
  Ports:
    Name  Port  Protocol
    ----  ----  --------
    http  8000  TCP

Events:  <none>
```

### Anything else we need to know?

The feature gate is enabled:

```
kubectl get --raw /metrics | grep kubernetes_feature_enabled | grep SidecarContainers
kubernetes_feature_enabled{name="SidecarContainers",stage="BETA"} 1
```

I was suggested to create this issue in [this Slack thread](https://kubernetes.slack.com/archives/C06JCTLFPFX/p1728475764781559)

Workarounds:

1) 

If we update the Service to contain numeric ports instead of named ports, they show up in the endpoint:
```diff
k diff -f reproduce.yaml
diff -u -N /tmp/LIVE-529307225/v1.Service.default.sidecar-ports /tmp/MERGED-1321372933/v1.Service.default.sidecar-ports
--- /tmp/LIVE-529307225/v1.Service.default.sidecar-ports 2024-10-09 14:40:30.518739432 +0200
+++ /tmp/MERGED-1321372933/v1.Service.default.sidecar-ports      2024-10-09 14:40:30.518739432 +0200
@@ -25,7 +25,7 @@
   - name: https
     port: 443
     protocol: TCP
-    targetPort: https
+    targetPort: 9443
   selector:
     app: sidecar-ports
   sessionAffinity: None
```

```
Name:         sidecar-ports
Namespace:    default
Subsets:
  Ports:
    Name   Port  Protocol
    ----   ----  --------
    https  9443  TCP
    http   8000  TCP

Events:  <none>
```

2)

If we specify the named port in the "main" container instead of in the initContainer, the port shows up in the endpoint:

```diff
spec:
  containers:
  - image: bloomberg/goldpinger:3.10.1
    name: goldpinger
    ports:
    - containerPort: 8000
      name: http
      protocol: TCP
+    - containerPort: 9443
+      name: https
+      protocol: TCP
  initContainers:
  # note: no envoy configuration is provided, the steps to reproduce involve reading other Kubernetes manifests (Endpoints)
  - image: envoyproxy/envoy:v1.73.7
    name: tls-terminator
-    ports:
-    - containerPort: 9443
-      name: https
-      protocol: TCP
```

```
Name:         sidecar-ports
Namespace:    default
Subsets:
  Ports:
    Name   Port  Protocol
    ----   ----  --------
    https  9443  TCP
    http   8000  TCP

Events:  <none>
```

### Kubernetes version

<details>

```console
$ kubectl version
Client Version: v1.29.9
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.29.8
```

</details>


### Cloud provider

<details>
Self-hosted, kubeadm-driven.
</details>


### OS version

<details>

```console
# On Linux:
$ cat /etc/os-release
I'll say Ubuntu.
$ uname -a
I can provide this if strictly relevant.

```

</details>


### Install tools

<details>

</details>


### Container runtime (CRI) and version (if applicable)

<details>
```
$ containerd --version
containerd containerd.io 1.7.20 8fc6bcff51318944179630522a095cc9dbf9f353
```
</details>


### Related plugins (CNI, CSI, ...) and versions (if applicable)

<details>

</details>



Discussion / hints:
/cc @SergeyKanzhelev @gjkim42 
/sig network
/sig node

Not sure which one is the best sig here. Let's discuss at triage
/triage accepted
/priority important-soon 

Fix patch:
diff --git a/pkg/api/v1/pod/util.go b/pkg/api/v1/pod/util.go
index 2bd37c5b5a14f..ba283df416386 100644
--- a/pkg/api/v1/pod/util.go
+++ b/pkg/api/v1/pod/util.go
@@ -41,6 +41,17 @@ func FindPort(pod *v1.Pod, svcPort *v1.ServicePort) (int, error) {
 				}
 			}
 		}
+		// also support sidecar container (initContainer with restartPolicy=Always)
+		for _, container := range pod.Spec.InitContainers {
+			if container.RestartPolicy == nil || *container.RestartPolicy != v1.ContainerRestartPolicyAlways {
+				continue
+			}
+			for _, port := range container.Ports {
+				if port.Name == name && port.Protocol == svcPort.Protocol {
+					return int(port.ContainerPort), nil
+				}
+			}
+		}
 	case intstr.Int:
 		return portName.IntValue(), nil
 	}


Test patch:
diff --git a/pkg/api/v1/pod/util_test.go b/pkg/api/v1/pod/util_test.go
index 225402fdaa77e..a172f8027427e 100644
--- a/pkg/api/v1/pod/util_test.go
+++ b/pkg/api/v1/pod/util_test.go
@@ -32,12 +32,14 @@ import (
 )
 
 func TestFindPort(t *testing.T) {
+	restartAlways := v1.ContainerRestartPolicyAlways
 	testCases := []struct {
-		name       string
-		containers []v1.Container
-		port       intstr.IntOrString
-		expected   int
-		pass       bool
+		name           string
+		containers     []v1.Container
+		initContainers []v1.Container
+		port           intstr.IntOrString
+		expected       int
+		pass           bool
 	}{{
 		name:       "valid int, no ports",
 		containers: []v1.Container{{}},
@@ -182,10 +184,69 @@ func TestFindPort(t *testing.T) {
 			expected: 11,
 			pass:     true,
 		},
+		{
+			name: "Sidecar initContainer named port",
+			initContainers: []v1.Container{{
+				RestartPolicy: &restartAlways,
+				Ports: []v1.ContainerPort{{
+					Name:          "a",
+					ContainerPort: 80,
+					HostPort:      -1,
+					Protocol:      "TCP",
+				}},
+			}},
+			port:     intstr.FromString("a"),
+			expected: 80,
+			pass:     true,
+		},
+		{
+			name: "Invalid(restartPolicy != Always) initContainer named port",
+			initContainers: []v1.Container{{
+				Ports: []v1.ContainerPort{{
+					Name:          "a",
+					ContainerPort: 80,
+					HostPort:      -1,
+					Protocol:      "TCP",
+				}},
+			}},
+			port:     intstr.FromString("a"),
+			expected: 0,
+			pass:     false,
+		},
+		{
+			name: "App and sidecar containers have the same named port, first app container port will be used",
+			initContainers: []v1.Container{{
+				RestartPolicy: &restartAlways,
+				Ports: []v1.ContainerPort{{
+					Name:          "a",
+					ContainerPort: 80,
+					HostPort:      -1,
+					Protocol:      "TCP",
+				}},
+			}},
+			containers: []v1.Container{{
+				Ports: []v1.ContainerPort{{
+					Name:          "a",
+					ContainerPort: 81,
+					HostPort:      -1,
+					Protocol:      "TCP",
+				}},
+			}, {
+				Ports: []v1.ContainerPort{{
+					Name:          "a",
+					ContainerPort: 82,
+					HostPort:      -1,
+					Protocol:      "TCP",
+				}},
+			}},
+			port:     intstr.FromString("a"),
+			expected: 81,
+			pass:     true,
+		},
 	}
 
 	for _, tc := range testCases {
-		port, err := FindPort(&v1.Pod{Spec: v1.PodSpec{Containers: tc.containers}},
+		port, err := FindPort(&v1.Pod{Spec: v1.PodSpec{Containers: tc.containers, InitContainers: tc.initContainers}},
 			&v1.ServicePort{Protocol: "TCP", TargetPort: tc.port})
 		if err != nil && tc.pass {
 			t.Errorf("unexpected error for %s: %v", tc.name, err)