Skip to content

Commit 6e016a2

Browse files
authored
fix: upgrade clustermanager/klusterlet when source repo changes (#75)
* fix: upgrade clustermanager/klusterlet when source repo changes Signed-off-by: Artur Shad Nik <[email protected]> * feat: dont nuke annotations Signed-off-by: Artur Shad Nik <[email protected]> * chore: handle all hashing algos Signed-off-by: Artur Shad Nik <[email protected]> --------- Signed-off-by: Artur Shad Nik <[email protected]>
1 parent 11c0c99 commit 6e016a2

File tree

7 files changed

+437
-9
lines changed

7 files changed

+437
-9
lines changed

fleetconfig-controller/charts/fleetconfig-controller/templates/ocm/fcc-addon/addon-template.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ spec:
9696
verbs: ["get", "list", "watch"]
9797
- apiGroups: ["apps"]
9898
resources: ["deployments"]
99-
verbs: ["get", "list", "watch"]
99+
verbs: ["get", "list", "watch", "patch"]
100100
- apiGroups: ["rbac.authorization.k8s.io"]
101101
resources: ["clusterrolebindings", "clusterroles"]
102102
verbs: ["get", "list", "watch"]

fleetconfig-controller/internal/controller/v1beta1/hub_controller.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ func (r *HubReconciler) hubNeedsUpgrade(ctx context.Context, hub *v1beta1.Hub, o
482482
if cm.Spec.WorkImagePullSpec != "" {
483483
bundleSpecs = append(bundleSpecs, cm.Spec.WorkImagePullSpec)
484484
}
485+
// bundle version changed
485486
activeBundleVersion, err := version.LowestBundleVersion(ctx, bundleSpecs)
486487
if err != nil {
487488
return false, fmt.Errorf("failed to detect bundleVersion from clustermanager spec: %w", err)
@@ -490,12 +491,24 @@ func (r *HubReconciler) hubNeedsUpgrade(ctx context.Context, hub *v1beta1.Hub, o
490491
if err != nil {
491492
return false, err
492493
}
494+
versionChanged := activeBundleVersion != desiredBundleVersion
495+
496+
// bundle source changed
497+
activeBundleSource, err := version.GetBundleSource(bundleSpecs)
498+
if err != nil {
499+
return false, fmt.Errorf("failed to get bundle source: %w", err)
500+
}
501+
desiredBundleSource := hub.Spec.ClusterManager.Source.Registry
502+
sourceChanged := activeBundleSource != desiredBundleSource
493503

494504
logger.V(0).Info("found clustermanager bundleVersions",
495505
"activeBundleVersion", activeBundleVersion,
496506
"desiredBundleVersion", desiredBundleVersion,
507+
"activeBundleSource", activeBundleSource,
508+
"desiredBundleSource", desiredBundleSource,
497509
)
498-
return activeBundleVersion != desiredBundleVersion, nil
510+
511+
return versionChanged || sourceChanged, nil
499512
}
500513

501514
// upgradeHub upgrades the Hub cluster's clustermanager to the specified version

fleetconfig-controller/internal/controller/v1beta1/spoke_handler.go

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
"maps"
99
"os"
1010
"os/exec"
11+
"reflect"
1112
"slices"
1213
"strconv"
14+
"strings"
1315

1416
"dario.cat/mergo"
1517
certificatesv1 "k8s.io/api/certificates/v1"
@@ -77,13 +79,25 @@ func (r *SpokeReconciler) handleSpoke(ctx context.Context, spoke *v1beta1.Spoke,
7779
return err
7880
}
7981

82+
// to avoid conflicts between sources, always use OCMSource as the source of truth for registry and version
83+
if klusterletValues != nil {
84+
if hubMeta.hub != nil && hubMeta.hub.Spec.ClusterManager != nil {
85+
if klusterletValues.Images.Registry != "" {
86+
klusterletValues.Images.Registry = hubMeta.hub.Spec.ClusterManager.Source.Registry
87+
}
88+
if klusterletValues.Images.Tag != "" {
89+
klusterletValues.Images.Tag = hubMeta.hub.Spec.ClusterManager.Source.BundleVersion
90+
}
91+
}
92+
}
93+
8094
switch r.InstanceType {
8195
case v1beta1.InstanceTypeManager:
8296
err = r.doHubWork(ctx, spoke, hubMeta, klusterletValues)
8397
if err != nil {
8498
return err
8599
}
86-
if spoke.IsHubAsSpoke() { // hub-as-spoke
100+
if spoke.IsHubAsSpoke() {
87101
err = r.doSpokeWork(ctx, spoke, hubMeta.hub, klusterletValues)
88102
if err != nil {
89103
spoke.SetConditions(true, v1beta1.NewCondition(
@@ -121,6 +135,57 @@ func (r *SpokeReconciler) handleSpoke(ctx context.Context, spoke *v1beta1.Spoke,
121135
}
122136
}
123137

138+
// merges annotation from Spoke spec and Klusterlet values overrides. For consistency with clusteradm, priority is given to klusterlet overrides.
139+
// since the behaviour w.r.t prefixes of the `--klusterlet-annotation` flag, and the annotations specified in `--klusterlet-values-file` are different,
140+
// this function will add the prefix to both before merging.
141+
// the output of this function is the complete and finalized set of annotations that will be applied to the ManagedCluster
142+
func mergeKlusterletAnnotations(base, override map[string]string) map[string]string {
143+
formattedBase := make(map[string]string, len(base))
144+
for k, v := range base {
145+
if !strings.HasPrefix(k, operatorv1.ClusterAnnotationsKeyPrefix) {
146+
k = fmt.Sprintf("%s/%s", operatorv1.ClusterAnnotationsKeyPrefix, k)
147+
}
148+
formattedBase[k] = v
149+
}
150+
formattedOverride := make(map[string]string, len(override))
151+
for k, v := range override {
152+
if !strings.HasPrefix(k, operatorv1.ClusterAnnotationsKeyPrefix) {
153+
k = fmt.Sprintf("%s/%s", operatorv1.ClusterAnnotationsKeyPrefix, k)
154+
}
155+
formattedOverride[k] = v
156+
}
157+
out := make(map[string]string, 0)
158+
maps.Copy(out, formattedBase)
159+
maps.Copy(out, formattedOverride)
160+
return out
161+
}
162+
163+
// syncManagedClusterAnnotations merges requested klusterlet annotations into the ManagedCluster's
164+
// existing annotations, preserving all non-klusterlet annotations while adding/updating/removing
165+
// only those with the klusterlet prefix.
166+
func syncManagedClusterAnnotations(current, requested map[string]string) map[string]string {
167+
if current == nil {
168+
current = map[string]string{}
169+
}
170+
171+
result := maps.Clone(current)
172+
prefix := operatorv1.ClusterAnnotationsKeyPrefix + "/"
173+
174+
// Remove klusterlet annotations that are no longer requested
175+
for key := range current {
176+
if strings.HasPrefix(key, prefix) {
177+
if _, stillWanted := requested[key]; !stillWanted {
178+
delete(result, key)
179+
}
180+
}
181+
}
182+
183+
// Add or update all requested klusterlet annotations
184+
maps.Copy(result, requested)
185+
186+
return result
187+
}
188+
124189
// doHubWork handles hub-side work such as joins and addons
125190
func (r *SpokeReconciler) doHubWork(ctx context.Context, spoke *v1beta1.Spoke, hubMeta hubMeta, klusterletValues *v1beta1.KlusterletChartConfig) error {
126191
logger := log.FromContext(ctx)
@@ -172,6 +237,23 @@ func (r *SpokeReconciler) doHubWork(ctx context.Context, spoke *v1beta1.Spoke, h
172237
}
173238
}
174239

240+
// TODO - handle this via `klusterlet upgrade` once https://github.com/open-cluster-management-io/ocm/issues/1210 is resolved
241+
if managedCluster != nil {
242+
klusterletValuesAnnotations := map[string]string{}
243+
if klusterletValues != nil {
244+
klusterletValuesAnnotations = klusterletValues.Klusterlet.RegistrationConfiguration.ClusterAnnotations
245+
}
246+
requestedAnnotations := mergeKlusterletAnnotations(spoke.Spec.Klusterlet.Annotations, klusterletValuesAnnotations)
247+
updatedAnnotations := syncManagedClusterAnnotations(managedCluster.GetAnnotations(), requestedAnnotations)
248+
if !reflect.DeepEqual(updatedAnnotations, managedCluster.GetAnnotations()) {
249+
managedCluster.SetAnnotations(updatedAnnotations)
250+
if err = common.UpdateManagedCluster(ctx, clusterClient, managedCluster); err != nil {
251+
return err
252+
}
253+
logger.V(1).Info("synced annotations to ManagedCluster")
254+
}
255+
}
256+
175257
// precreate the namespace that the agent will be installed into
176258
// this prevents it from being automatically garbage collected when the spoke is deregistered
177259
err = r.createAgentNamespace(ctx, spoke)
@@ -346,6 +428,7 @@ func (r *SpokeReconciler) doSpokeWork(ctx context.Context, spoke *v1beta1.Spoke,
346428
if err != nil {
347429
return fmt.Errorf("failed to load kubeconfig from inCluster: %v", err)
348430
}
431+
349432
// attempt an upgrade whenever the klusterlet's bundleVersion or values change
350433
currKlusterletHash, err := hash.ComputeHash(klusterletValues)
351434
if err != nil {
@@ -663,7 +746,6 @@ func (r *SpokeReconciler) joinSpoke(ctx context.Context, spoke *v1beta1.Spoke, h
663746
for k, v := range spoke.Spec.Klusterlet.Annotations {
664747
joinArgs = append(joinArgs, fmt.Sprintf("--klusterlet-annotation=%s=%s", k, v))
665748
}
666-
667749
// resources args
668750
joinArgs = append(joinArgs, arg_utils.PrepareResources(spoke.Spec.Klusterlet.Resources)...)
669751

@@ -813,6 +895,7 @@ func (r *SpokeReconciler) spokeNeedsUpgrade(ctx context.Context, spoke *v1beta1.
813895
logger := log.FromContext(ctx)
814896
logger.V(0).Info("spokeNeedsUpgrade", "spokeClusterName", spoke.Name)
815897

898+
// klusterlet values hash changed
816899
prevHash := spoke.Status.KlusterletHash
817900
hashChanged := prevHash != currKlusterletHash && prevHash != ""
818901
logger.V(2).Info("comparing klusterlet values hash",
@@ -855,6 +938,8 @@ func (r *SpokeReconciler) spokeNeedsUpgrade(ctx context.Context, spoke *v1beta1.
855938
if k.Spec.WorkImagePullSpec != "" {
856939
bundleSpecs = append(bundleSpecs, k.Spec.WorkImagePullSpec)
857940
}
941+
942+
// bundle version changed
858943
activeBundleVersion, err := version.LowestBundleVersion(ctx, bundleSpecs)
859944
if err != nil {
860945
return false, fmt.Errorf("failed to detect bundleVersion from klusterlet spec: %w", err)
@@ -863,12 +948,24 @@ func (r *SpokeReconciler) spokeNeedsUpgrade(ctx context.Context, spoke *v1beta1.
863948
if err != nil {
864949
return false, err
865950
}
951+
versionChanged := activeBundleVersion != desiredBundleVersion
952+
953+
// bundle source changed
954+
activeBundleSource, err := version.GetBundleSource(bundleSpecs)
955+
if err != nil {
956+
return false, fmt.Errorf("failed to get bundle source: %w", err)
957+
}
958+
desiredBundleSource := source.Registry
959+
sourceChanged := activeBundleSource != desiredBundleSource
866960

867961
logger.V(0).Info("found klusterlet bundleVersions",
868962
"activeBundleVersion", activeBundleVersion,
869963
"desiredBundleVersion", desiredBundleVersion,
964+
"activeBundleSource", activeBundleSource,
965+
"desiredBundleSource", desiredBundleSource,
870966
)
871-
return activeBundleVersion != desiredBundleVersion, nil
967+
968+
return versionChanged || sourceChanged, nil
872969
}
873970

874971
// upgradeSpoke upgrades the Spoke cluster's klusterlet
@@ -1075,7 +1172,6 @@ func (r *SpokeReconciler) mergeKlusterletValues(ctx context.Context, spoke *v1be
10751172
}
10761173

10771174
return merged, nil
1078-
10791175
}
10801176

10811177
// prepareKlusterletValuesFile creates a temporary file with klusterlet values and returns
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package v1beta1
2+
3+
import (
4+
"testing"
5+
6+
operatorv1 "open-cluster-management.io/api/operator/v1"
7+
)
8+
9+
func TestSyncManagedClusterAnnotations(t *testing.T) {
10+
const prefix = operatorv1.ClusterAnnotationsKeyPrefix + "/"
11+
12+
tests := []struct {
13+
name string
14+
current map[string]string
15+
requested map[string]string
16+
want map[string]string
17+
}{
18+
{
19+
name: "preserve non-klusterlet annotations",
20+
current: map[string]string{
21+
"other.io/annotation": "keep-me",
22+
"another.io/annotation": "also-keep",
23+
prefix + "klusterlet-key": "old-value",
24+
},
25+
requested: map[string]string{
26+
prefix + "klusterlet-key": "new-value",
27+
},
28+
want: map[string]string{
29+
"other.io/annotation": "keep-me",
30+
"another.io/annotation": "also-keep",
31+
prefix + "klusterlet-key": "new-value",
32+
},
33+
},
34+
{
35+
name: "add new klusterlet annotations",
36+
current: map[string]string{
37+
"other.io/annotation": "keep-me",
38+
prefix + "existing-key": "existing-value",
39+
},
40+
requested: map[string]string{
41+
prefix + "existing-key": "existing-value",
42+
prefix + "new-key": "new-value",
43+
},
44+
want: map[string]string{
45+
"other.io/annotation": "keep-me",
46+
prefix + "existing-key": "existing-value",
47+
prefix + "new-key": "new-value",
48+
},
49+
},
50+
{
51+
name: "update existing klusterlet annotations",
52+
current: map[string]string{
53+
"other.io/annotation": "keep-me",
54+
prefix + "key1": "old-value-1",
55+
prefix + "key2": "old-value-2",
56+
},
57+
requested: map[string]string{
58+
prefix + "key1": "new-value-1",
59+
prefix + "key2": "new-value-2",
60+
},
61+
want: map[string]string{
62+
"other.io/annotation": "keep-me",
63+
prefix + "key1": "new-value-1",
64+
prefix + "key2": "new-value-2",
65+
},
66+
},
67+
{
68+
name: "remove klusterlet annotations that are no longer requested",
69+
current: map[string]string{
70+
"other.io/annotation": "keep-me",
71+
prefix + "keep-key": "keep-value",
72+
prefix + "remove-key1": "will-be-removed",
73+
prefix + "remove-key2": "also-removed",
74+
},
75+
requested: map[string]string{
76+
prefix + "keep-key": "keep-value",
77+
},
78+
want: map[string]string{
79+
"other.io/annotation": "keep-me",
80+
prefix + "keep-key": "keep-value",
81+
},
82+
},
83+
{
84+
name: "handle nil current annotations",
85+
current: nil,
86+
requested: map[string]string{
87+
prefix + "new-key": "new-value",
88+
},
89+
want: map[string]string{
90+
prefix + "new-key": "new-value",
91+
},
92+
},
93+
{
94+
name: "handle empty current annotations",
95+
current: map[string]string{},
96+
requested: map[string]string{
97+
prefix + "new-key": "new-value",
98+
},
99+
want: map[string]string{
100+
prefix + "new-key": "new-value",
101+
},
102+
},
103+
{
104+
name: "handle empty requested annotations",
105+
current: map[string]string{
106+
"other.io/annotation": "keep-me",
107+
prefix + "remove-key1": "will-be-removed",
108+
prefix + "remove-key2": "also-removed",
109+
},
110+
requested: map[string]string{},
111+
want: map[string]string{
112+
"other.io/annotation": "keep-me",
113+
},
114+
},
115+
{
116+
name: "complex scenario with add, update, remove, and preserve",
117+
current: map[string]string{
118+
"other.io/annotation": "keep-me",
119+
"third-party.io/annotation": "also-keep",
120+
prefix + "update-me": "old-value",
121+
prefix + "keep-me": "keep-value",
122+
prefix + "remove-me": "will-be-removed",
123+
},
124+
requested: map[string]string{
125+
prefix + "update-me": "new-value",
126+
prefix + "keep-me": "keep-value",
127+
prefix + "add-me": "new-annotation",
128+
},
129+
want: map[string]string{
130+
"other.io/annotation": "keep-me",
131+
"third-party.io/annotation": "also-keep",
132+
prefix + "update-me": "new-value",
133+
prefix + "keep-me": "keep-value",
134+
prefix + "add-me": "new-annotation",
135+
},
136+
},
137+
}
138+
139+
for _, tt := range tests {
140+
t.Run(tt.name, func(t *testing.T) {
141+
got := syncManagedClusterAnnotations(tt.current, tt.requested)
142+
143+
if len(got) != len(tt.want) {
144+
t.Errorf("syncManagedClusterAnnotations() returned %d annotations, want %d", len(got), len(tt.want))
145+
}
146+
147+
for key, wantValue := range tt.want {
148+
gotValue, ok := got[key]
149+
if !ok {
150+
t.Errorf("syncManagedClusterAnnotations() missing key %q", key)
151+
continue
152+
}
153+
if gotValue != wantValue {
154+
t.Errorf("syncManagedClusterAnnotations() key %q = %q, want %q", key, gotValue, wantValue)
155+
}
156+
}
157+
158+
for key := range got {
159+
if _, ok := tt.want[key]; !ok {
160+
t.Errorf("syncManagedClusterAnnotations() has unexpected key %q with value %q", key, got[key])
161+
}
162+
}
163+
})
164+
}
165+
}

0 commit comments

Comments
 (0)