From 53361ff47e62b65be34c482a6b70041d801de703 Mon Sep 17 00:00:00 2001 From: Rahul Date: Wed, 22 Apr 2026 13:48:04 -0700 Subject: [PATCH] fix(endpoint): avoid panic on services with empty IPFamilies Accessing svc.Spec.IPFamilies[0] without a bounds check panics when a service reaches the controller with an empty IPFamilies field. This can happen via watch events: the apiserver's defaultOnRead decorator populates IPFamilies on GET/LIST but not on watch (cachingObject wrapping bypasses the type assertion). Restore the inference logic removed in #130101: fall back to ClusterIP for headful services and pod IP for headless services. Signed-off-by: Rahul --- .../endpoint/endpoints_controller.go | 16 +++- .../endpoint/endpoints_controller_test.go | 80 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/pkg/controller/endpoint/endpoints_controller.go b/pkg/controller/endpoint/endpoints_controller.go index fe1c5f40832..6b52f860a2f 100644 --- a/pkg/controller/endpoint/endpoints_controller.go +++ b/pkg/controller/endpoint/endpoints_controller.go @@ -220,8 +220,22 @@ func (e *Controller) Run(ctx context.Context, workers int) { func podToEndpointAddressForService(svc *v1.Service, pod *v1.Pod) (*v1.EndpointAddress, error) { var endpointIP string + ipFamily := v1.IPv4Protocol - wantIPv6 := svc.Spec.IPFamilies[0] == v1.IPv6Protocol + // IPFamilies is expected to be populated by apiserver defaulting, but + // some services may reach this controller with an empty IPFamilies via + // watch events. Infer the family from ClusterIP or + // pod IP so we never panic on IPFamilies[0]. + if len(svc.Spec.IPFamilies) > 0 { + ipFamily = svc.Spec.IPFamilies[0] + } else if len(svc.Spec.ClusterIP) > 0 && svc.Spec.ClusterIP != v1.ClusterIPNone { + if utilnet.IsIPv6String(svc.Spec.ClusterIP) { + ipFamily = v1.IPv6Protocol + } + } else if utilnet.IsIPv6String(pod.Status.PodIP) { + ipFamily = v1.IPv6Protocol + } + wantIPv6 := ipFamily == v1.IPv6Protocol // Find an IP that matches the family. We parse and restringify the IP in case the // value on the Pod is in an irregular format. diff --git a/pkg/controller/endpoint/endpoints_controller_test.go b/pkg/controller/endpoint/endpoints_controller_test.go index bead9c2f649..c12fb83a9fa 100644 --- a/pkg/controller/endpoint/endpoints_controller_test.go +++ b/pkg/controller/endpoint/endpoints_controller_test.go @@ -3224,3 +3224,83 @@ func TestSyncEndpointsAddDeletePorts(t *testing.T) { t.Fatalf("incorrect endpoints after deleting first port:\n%s", diff) } } + +func TestPodToEndpointAddressForServiceEmptyIPFamilies(t *testing.T) { + testCases := []struct { + name string + clusterIP string + podIPs []v1.PodIP + podIP string + wantErr bool + wantFamily v1.IPFamily + }{ + { + name: "headful IPv4, IPv4 pod", + clusterIP: "10.0.0.1", + podIPs: []v1.PodIP{{IP: "10.244.0.1"}}, + wantFamily: v1.IPv4Protocol, + }, + { + name: "headful IPv6, IPv6 pod", + clusterIP: "fd00::1", + podIPs: []v1.PodIP{{IP: "fd00::10"}}, + wantFamily: v1.IPv6Protocol, + }, + { + name: "headful IPv4, no matching pod IP", + clusterIP: "10.0.0.1", + podIPs: []v1.PodIP{{IP: "fd00::10"}}, + wantErr: true, + }, + { + name: "headless, IPv4 pod", + clusterIP: v1.ClusterIPNone, + podIPs: []v1.PodIP{{IP: "10.244.0.1"}}, + podIP: "10.244.0.1", + wantFamily: v1.IPv4Protocol, + }, + { + name: "headless, IPv6 pod", + clusterIP: v1.ClusterIPNone, + podIPs: []v1.PodIP{{IP: "fd00::10"}}, + podIP: "fd00::10", + wantFamily: v1.IPv6Protocol, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, + Spec: v1.ServiceSpec{ + // Intentionally leave IPFamilies empty. + ClusterIP: tc.clusterIP, + }, + } + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "foo-pod", Namespace: "bar", UID: "uid-1"}, + Spec: v1.PodSpec{NodeName: "node-1"}, + Status: v1.PodStatus{PodIP: tc.podIP, PodIPs: tc.podIPs}, + } + + addr, err := podToEndpointAddressForService(svc, pod) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error but got addr=%v", addr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if addr == nil { + t.Fatal("expected an address but got nil") + } + isV6 := utilnet.IsIPv6String(addr.IP) + wantV6 := tc.wantFamily == v1.IPv6Protocol + if isV6 != wantV6 { + t.Errorf("got IP %q (IPv6=%v), want family %v", addr.IP, isV6, tc.wantFamily) + } + }) + } +}