diff --git a/changelogs/unreleased/9898-kaovilai b/changelogs/unreleased/9898-kaovilai new file mode 100644 index 0000000000..f2675c10d8 --- /dev/null +++ b/changelogs/unreleased/9898-kaovilai @@ -0,0 +1 @@ +Make ToSystemAffinity deterministic by sorting MatchLabels keys to avoid spurious affinity spec diffs and restarts diff --git a/pkg/util/kube/pod.go b/pkg/util/kube/pod.go index 4dc423272a..719fa5e23f 100644 --- a/pkg/util/kube/pod.go +++ b/pkg/util/kube/pod.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "os" + "sort" "strings" "time" @@ -236,7 +237,20 @@ func CollectPodLogs(ctx context.Context, podGetter corev1client.CoreV1Interface, func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopology *corev1api.NodeSelector) *corev1api.Affinity { requirements := []corev1api.NodeSelectorRequirement{} if loadAffinity != nil { - for k, v := range loadAffinity.NodeSelector.MatchLabels { + // MatchLabels is a map, so its iteration order is not deterministic. + // Sort the keys so the generated requirements (and therefore the + // resulting affinity) have a stable order. This output may be embedded + // into objects that are reconciled continuously (e.g. DaemonSet pod + // templates), where an order-only difference would be treated as a spec + // change and trigger unnecessary rollouts/restarts. + keys := make([]string, 0, len(loadAffinity.NodeSelector.MatchLabels)) + for k := range loadAffinity.NodeSelector.MatchLabels { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := loadAffinity.NodeSelector.MatchLabels[k] requirements = append(requirements, corev1api.NodeSelectorRequirement{ Key: k, Values: []string{v}, diff --git a/pkg/util/kube/pod_test.go b/pkg/util/kube/pod_test.go index aa8d4db991..0509a90891 100644 --- a/pkg/util/kube/pod_test.go +++ b/pkg/util/kube/pod_test.go @@ -834,6 +834,45 @@ func TestToSystemAffinity(t *testing.T) { }, }, }, + { + name: "with multiple match labels are sorted by key", + loadAffinity: &LoadAffinity{ + NodeSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key-c": "value-c", + "key-a": "value-a", + "key-b": "value-b", + }, + }, + }, + expected: &corev1api.Affinity{ + NodeAffinity: &corev1api.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{ + NodeSelectorTerms: []corev1api.NodeSelectorTerm{ + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-a", + Values: []string{"value-a"}, + Operator: corev1api.NodeSelectorOpIn, + }, + { + Key: "key-b", + Values: []string{"value-b"}, + Operator: corev1api.NodeSelectorOpIn, + }, + { + Key: "key-c", + Values: []string{"value-c"}, + Operator: corev1api.NodeSelectorOpIn, + }, + }, + }, + }, + }, + }, + }, + }, { name: "with olume topology", volumeTopology: &corev1api.NodeSelector{