fix: keep annotations flag ordering consistent on each reconcile (#1644)

Signed-off-by: Artur Shad Nik <arturshadnik@gmail.com>
This commit is contained in:
Artur Shad Nik
2026-07-31 00:33:19 +00:00
committed by GitHub
parent 5ef31d711f
commit ac59d84dc7
2 changed files with 82 additions and 6 deletions
@@ -3,6 +3,7 @@ package klusterletcontroller
import (
"context"
"fmt"
"sort"
"strings"
"time"
@@ -58,6 +59,19 @@ var operatorOnlyFeatureGates = map[featuregate.Feature]bool{
NetworkPolicies: true,
}
// buildClusterAnnotationsString renders the ClusterAnnotations map into the
// comma-separated "key1=value1,key2=value2" form consumed by the registration
// agent's --cluster-annotations flag. Keys are sorted deterministically.
func buildClusterAnnotationsString(annotations map[string]string) string {
filtered := commonhelpers.FilterClusterAnnotations(annotations)
arr := make([]string, 0, len(filtered))
for k, v := range filtered {
arr = append(arr, fmt.Sprintf("%s=%s", k, v))
}
sort.Strings(arr)
return strings.Join(arr, ",")
}
func filterOperatorFeatureGates(features []operatorapiv1.FeatureGate) []operatorapiv1.FeatureGate {
var filtered []operatorapiv1.FeatureGate
for _, f := range features {
@@ -422,12 +436,7 @@ func (n *klusterletController) sync(ctx context.Context, controllerContext facto
klusterlet.Spec.RegistrationConfiguration.ClusterClaimConfiguration.ReservedClusterClaimSuffixes, ",")
}
// construct cluster annotations string, the final format is "key1=value1,key2=value2"
var annotationsArray []string
for k, v := range commonhelpers.FilterClusterAnnotations(klusterlet.Spec.RegistrationConfiguration.ClusterAnnotations) {
annotationsArray = append(annotationsArray, fmt.Sprintf("%s=%s", k, v))
}
config.ClusterAnnotationsString = strings.Join(annotationsArray, ",")
config.ClusterAnnotationsString = buildClusterAnnotationsString(klusterlet.Spec.RegistrationConfiguration.ClusterAnnotations)
// Set AddOnKubeClientRegistrationAuth from the Klusterlet spec
if klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver != nil &&
@@ -1917,3 +1917,70 @@ func TestCleanWithMultipleKlusterletAgentNamespaces(t *testing.T) {
})
}
}
// TestBuildClusterAnnotationsString verifies that the annotations flag string has consistent ordering across multiple runs.
func TestBuildClusterAnnotationsString(t *testing.T) {
const prefix = "agent.open-cluster-management.io"
tests := []struct {
name string
annotations map[string]string
want string
}{
{
name: "nil map",
annotations: nil,
want: "",
},
{
name: "empty map",
annotations: map[string]string{},
want: "",
},
{
name: "single key",
annotations: map[string]string{prefix + "/foo": "bar"},
want: prefix + "/foo=bar",
},
{
name: "keys sorted lexicographically",
annotations: map[string]string{
prefix + "/c": "3",
prefix + "/a": "1",
prefix + "/b": "2",
},
want: prefix + "/a=1," + prefix + "/b=2," + prefix + "/c=3",
},
{
name: "non-prefixed keys are filtered out",
annotations: map[string]string{
prefix + "/keep": "yes",
"other.example/drop": "no",
},
want: prefix + "/keep=yes",
},
{
name: "many keys stable across many invocations",
annotations: map[string]string{
prefix + "/a": "1",
prefix + "/b": "2",
prefix + "/c": "3",
prefix + "/d": "4",
prefix + "/e": "5",
prefix + "/f": "6",
},
want: prefix + "/a=1," + prefix + "/b=2," + prefix + "/c=3," + prefix + "/d=4," + prefix + "/e=5," + prefix + "/f=6",
},
}
const iterations = 1000
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for i := range iterations {
got := buildClusterAnnotationsString(tt.annotations)
if got != tt.want {
t.Fatalf("iteration %d: got %q, want %q (non-deterministic output regresses cluster-annotations flag)", i, got, tt.want)
}
}
})
}
}