From 51d6db57c8d308113bc67c97c477cae28d1549a2 Mon Sep 17 00:00:00 2001 From: YuMin Kim Date: Sun, 1 Nov 2026 01:40:06 +0900 Subject: [PATCH] sort node readiness label keys when generating node affinity Iterating over the node_readiness_label map produced the match expressions in random order. With two or more labels the generated affinity differed from the one in the running statefulset on every sync, so the operator kept replacing the statefulset and rolling the pods. Signed-off-by: YuMin Kim --- pkg/cluster/k8sres.go | 5 +++-- pkg/cluster/k8sres_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 2353e464f..6ce424df0 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -516,11 +516,12 @@ func (c *Cluster) nodeAffinity(nodeReadinessLabel map[string]string, nodeAffinit } if len(nodeReadinessLabel) > 0 { matchExpressions := make([]v1.NodeSelectorRequirement, 0) - for k, v := range nodeReadinessLabel { + // sorted keys keep the generated affinity stable between syncs + for _, k := range slices.Sorted(maps.Keys(nodeReadinessLabel)) { matchExpressions = append(matchExpressions, v1.NodeSelectorRequirement{ Key: k, Operator: v1.NodeSelectorOpIn, - Values: []string{v}, + Values: []string{nodeReadinessLabel[k]}, }) } nodeReadinessSelectorTerm := v1.NodeSelectorTerm{MatchExpressions: matchExpressions} diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 3200dc2e7..f24a6e5f1 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -1514,6 +1514,30 @@ func TestNodeAffinity(t *testing.T) { assert.Equal(t, s.Spec.Template.Spec.Affinity.NodeAffinity, nodeAff, "cluster template has correct node affinity") } +func TestNodeAffinityReadinessLabelOrder(t *testing.T) { + cluster := New(Config{}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder) + nodeReadinessLabel := map[string]string{ + "lifecycle-status": "ready", + "nodepool": "platform", + "postgres_ready": "true", + "zone": "a", + } + + expected := []v1.NodeSelectorRequirement{ + {Key: "lifecycle-status", Operator: v1.NodeSelectorOpIn, Values: []string{"ready"}}, + {Key: "nodepool", Operator: v1.NodeSelectorOpIn, Values: []string{"platform"}}, + {Key: "postgres_ready", Operator: v1.NodeSelectorOpIn, Values: []string{"true"}}, + {Key: "zone", Operator: v1.NodeSelectorOpIn, Values: []string{"a"}}, + } + + // map iteration order is random, so an unsorted result would differ between syncs + for i := 0; i < 20; i++ { + affinity := cluster.nodeAffinity(nodeReadinessLabel, nil) + terms := affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + assert.Equal(t, expected, terms[0].MatchExpressions, "node readiness match expressions should be sorted by key") + } +} + func TestPodAffinity(t *testing.T) { clusterName := "acid-test-cluster" namespace := "default"