@@ -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
125190func (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
0 commit comments