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 <rahulbabu95@gmail.com>
This commit is contained in:
Rahul 2026-04-22 13:48:04 -07:00
parent 33576824d7
commit 53361ff47e
No known key found for this signature in database
2 changed files with 95 additions and 1 deletions

View file

@ -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.

View file

@ -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)
}
})
}
}