Merge pull request #136502 from tzneal/fix-endpointslice-churn-for-headless-services

fix: normalize nil ports to empty slice in NewPortMapKey
This commit is contained in:
Kubernetes Prow Robot 2026-01-28 00:05:49 +05:30 committed by GitHub
commit f2b4e9f9c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 0 deletions

View file

@ -2209,6 +2209,49 @@ func TestReconcile_TrafficDistribution(t *testing.T) {
}
}
// TestReconcileHeadlessServiceNoPorts verifies that headless services with no ports
// don't cause EndpointSlice churn. Validates fix for https://github.com/kubernetes/kubernetes/issues/133474
func TestReconcileHeadlessServiceNoPorts(t *testing.T) {
namespace := "test"
client := newClientset()
setupMetrics()
svc := corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "headless-no-ports",
Namespace: namespace,
UID: "test-uid",
},
Spec: corev1.ServiceSpec{
ClusterIP: corev1.ClusterIPNone,
Selector: map[string]string{"foo": "bar"},
IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol},
},
}
pod := newPod(1, namespace, true, 1, false)
r := newReconciler(client, []*corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: pod.Spec.NodeName}}}, defaultMaxEndpointsPerSlice)
reconcileHelper(t, r, &svc, []*corev1.Pod{pod}, []*discovery.EndpointSlice{}, time.Now())
assert.Len(t, client.Actions(), 1, "Expected 1 additional clientset action")
expectActions(t, client.Actions(), 1, "create", "endpointslices")
var existingSlices []*discovery.EndpointSlice
for _, slice := range fetchEndpointSlices(t, client, namespace) {
copy := slice.DeepCopy()
// replicate API server behavior which serializes this empty slice as nil
copy.Ports = nil
existingSlices = append(existingSlices, copy)
}
assert.Len(t, existingSlices, 1, "Expected 1 endpoint slices")
reconcileHelper(t, r, &svc, []*corev1.Pod{pod}, existingSlices, time.Now())
assert.Len(t, client.Actions(), 2, "Expected second reconcile to only list")
expectActions(t, client.Actions(), 1, "list", "endpointslices")
}
// Test Helpers
func newReconciler(client *fake.Clientset, nodes []*corev1.Node, maxEndpointsPerSlice int32) *Reconciler {

View file

@ -159,6 +159,10 @@ type PortMapKey string
// NewPortMapKey generates a PortMapKey from endpoint ports.
func NewPortMapKey(endpointPorts []discovery.EndpointPort) PortMapKey {
// Normalize nil to empty slice so they hash the same.
if endpointPorts == nil {
endpointPorts = []discovery.EndpointPort{}
}
sort.Sort(portsInOrder(endpointPorts))
return PortMapKey(deepHashObjectToString(endpointPorts))
}

View file

@ -948,3 +948,15 @@ func TestDeepObjectPointer(t *testing.T) {
}
}
}
func TestNewPortMapKey_NilVsEmptySlice(t *testing.T) {
var nilPorts []discovery.EndpointPort
emptyPorts := []discovery.EndpointPort{}
nilKey := NewPortMapKey(nilPorts)
emptyKey := NewPortMapKey(emptyPorts)
if nilKey != emptyKey {
t.Errorf("NewPortMapKey should return the same key for nil and empty slice, got nil=%q empty=%q", nilKey, emptyKey)
}
}