@@ -13,6 +13,7 @@ import (
1313 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1414 "k8s.io/apimachinery/pkg/types"
1515 utilruntime "k8s.io/apimachinery/pkg/util/runtime"
16+ "k8s.io/apimachinery/pkg/util/sets"
1617 "k8s.io/apimachinery/pkg/util/wait"
1718 informerscorev1 "k8s.io/client-go/informers/core/v1"
1819 "k8s.io/client-go/kubernetes"
@@ -120,6 +121,7 @@ type Operator struct {
120121 cmConfigLister listerscorev1.ConfigMapNamespaceLister
121122 cmConfigManagedLister listerscorev1.ConfigMapNamespaceLister
122123 proxyLister configlistersv1.ProxyLister
124+ featureGateLister configlistersv1.FeatureGateLister
123125 cacheSynced []cache.InformerSynced
124126
125127 // queue tracks applying updates to a cluster.
@@ -189,6 +191,10 @@ type Operator struct {
189191 // featurechangestopper controller will detect when cluster feature gate config changes and shutdown the CVO.
190192 enabledFeatureGates featuregates.CvoGateChecker
191193
194+ // featureGatesMutex protects access to enabledManifestFeatureGates
195+ featureGatesMutex sync.RWMutex
196+ enabledManifestFeatureGates sets.Set [string ]
197+
192198 clusterProfile string
193199 uid types.UID
194200
@@ -213,6 +219,7 @@ func New(
213219 cmConfigManagedInformer informerscorev1.ConfigMapInformer ,
214220 proxyInformer configinformersv1.ProxyInformer ,
215221 operatorInformerFactory operatorexternalversions.SharedInformerFactory ,
222+ featureGateInformer configinformersv1.FeatureGateInformer ,
216223 client clientset.Interface ,
217224 kubeClient kubernetes.Interface ,
218225 operatorClient operatorclientset.Interface ,
@@ -225,6 +232,7 @@ func New(
225232 alwaysEnableCapabilities []configv1.ClusterVersionCapability ,
226233 featureSet configv1.FeatureSet ,
227234 cvoGates featuregates.CvoGateChecker ,
235+ startingEnabledManifestFeatureGates sets.Set [string ],
228236) (* Operator , error ) {
229237 eventBroadcaster := record .NewBroadcaster ()
230238 eventBroadcaster .StartLogging (klog .Infof )
@@ -248,18 +256,19 @@ func New(
248256 kubeClient : kubeClient ,
249257 operatorClient : operatorClient ,
250258 eventRecorder : eventBroadcaster .NewRecorder (scheme .Scheme , corev1.EventSource {Component : namespace }),
251- queue : workqueue .NewTypedRateLimitingQueueWithConfig [ any ] (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "clusterversion" }),
252- availableUpdatesQueue : workqueue .NewTypedRateLimitingQueueWithConfig [ any ] (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "availableupdates" }),
253- upgradeableQueue : workqueue .NewTypedRateLimitingQueueWithConfig [ any ] (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "upgradeable" }),
259+ queue : workqueue .NewTypedRateLimitingQueueWithConfig (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "clusterversion" }),
260+ availableUpdatesQueue : workqueue .NewTypedRateLimitingQueueWithConfig (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "availableupdates" }),
261+ upgradeableQueue : workqueue .NewTypedRateLimitingQueueWithConfig (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "upgradeable" }),
254262
255263 hypershift : hypershift ,
256264 exclude : exclude ,
257265 clusterProfile : clusterProfile ,
258266 conditionRegistry : standard .NewConditionRegistry (promqlTarget ),
259267 injectClusterIdIntoPromQL : injectClusterIdIntoPromQL ,
260268
261- requiredFeatureSet : featureSet ,
262- enabledFeatureGates : cvoGates ,
269+ requiredFeatureSet : featureSet ,
270+ enabledFeatureGates : cvoGates ,
271+ enabledManifestFeatureGates : startingEnabledManifestFeatureGates ,
263272
264273 alwaysEnableCapabilities : alwaysEnableCapabilities ,
265274 }
@@ -276,6 +285,9 @@ func New(
276285 if _ , err := coInformer .Informer ().AddEventHandler (optr .clusterOperatorEventHandler ()); err != nil {
277286 return nil , err
278287 }
288+ if _ , err := featureGateInformer .Informer ().AddEventHandler (optr .featureGateEventHandler ()); err != nil {
289+ return nil , err
290+ }
279291
280292 optr .coLister = coInformer .Lister ()
281293 optr .cacheSynced = append (optr .cacheSynced , coInformer .Informer ().HasSynced )
@@ -287,6 +299,9 @@ func New(
287299 optr .cmConfigLister = cmConfigInformer .Lister ().ConfigMaps (internal .ConfigNamespace )
288300 optr .cmConfigManagedLister = cmConfigManagedInformer .Lister ().ConfigMaps (internal .ConfigManagedNamespace )
289301
302+ optr .featureGateLister = featureGateInformer .Lister ()
303+ optr .cacheSynced = append (optr .cacheSynced , featureGateInformer .Informer ().HasSynced )
304+
290305 // make sure this is initialized after all the listers are initialized
291306 optr .upgradeableChecks = optr .defaultUpgradeableChecks ()
292307
@@ -318,7 +333,7 @@ func (optr *Operator) LoadInitialPayload(ctx context.Context, restConfig *rest.C
318333 }
319334
320335 update , err := payload .LoadUpdate (optr .defaultPayloadDir (), optr .release .Image , optr .exclude , string (optr .requiredFeatureSet ),
321- optr .clusterProfile , configv1 .KnownClusterVersionCapabilities )
336+ optr .clusterProfile , configv1 .KnownClusterVersionCapabilities , optr . getEnabledFeatureGates () )
322337
323338 if err != nil {
324339 return nil , fmt .Errorf ("the local release contents are invalid - no current version can be determined from disk: %v" , err )
@@ -779,7 +794,7 @@ func (optr *Operator) sync(ctx context.Context, key string) error {
779794 }
780795
781796 // inform the config sync loop about our desired state
782- status := optr .configSync .Update (ctx , config .Generation , desired , config , state )
797+ status := optr .configSync .Update (ctx , config .Generation , desired , config , state , optr . getEnabledFeatureGates () )
783798
784799 // write cluster version status
785800 return optr .syncStatus (ctx , original , config , status , errs )
@@ -1084,6 +1099,72 @@ func (optr *Operator) HTTPClient() (*http.Client, error) {
10841099 }, nil
10851100}
10861101
1102+ // featureGateEventHandler handles changes to FeatureGate objects and updates the cluster feature gates
1103+ func (optr * Operator ) featureGateEventHandler () cache.ResourceEventHandler {
1104+ workQueueKey := optr .queueKey ()
1105+ return cache.ResourceEventHandlerFuncs {
1106+ AddFunc : func (obj interface {}) {
1107+ if optr .updateEnabledFeatureGates (obj ) {
1108+ optr .queue .Add (workQueueKey )
1109+ }
1110+ },
1111+ UpdateFunc : func (old , new interface {}) {
1112+ if optr .updateEnabledFeatureGates (new ) {
1113+ optr .queue .Add (workQueueKey )
1114+ }
1115+ },
1116+ }
1117+ }
1118+
1119+ // updateEnabledFeatureGates updates the cluster feature gates based on a FeatureGate object.
1120+ // Returns true or false based on whether or not the gates were actually updated.
1121+ // This allows us to avoid unnecessary work if the gates have not changed.
1122+ func (optr * Operator ) updateEnabledFeatureGates (obj interface {}) bool {
1123+ featureGate , ok := obj .(* configv1.FeatureGate )
1124+ if ! ok {
1125+ klog .Warningf ("Expected FeatureGate object but got %T" , obj )
1126+ return false
1127+ }
1128+
1129+ newGates := optr .extractEnabledGates (featureGate )
1130+
1131+ optr .featureGatesMutex .Lock ()
1132+ defer optr .featureGatesMutex .Unlock ()
1133+
1134+ // Check if gates actually changed to avoid unnecessary work
1135+ if ! optr .enabledManifestFeatureGates .Equal (newGates ) {
1136+
1137+ klog .V (2 ).Infof ("Cluster feature gates changed from %v to %v" ,
1138+ sets .List (optr .enabledManifestFeatureGates ), sets .List (newGates ))
1139+
1140+ optr .enabledManifestFeatureGates = newGates
1141+ return true
1142+ }
1143+
1144+ return false
1145+ }
1146+
1147+ // getEnabledFeatureGates returns a copy of the current cluster feature gates for safe consumption
1148+ func (optr * Operator ) getEnabledFeatureGates () sets.Set [string ] {
1149+ optr .featureGatesMutex .RLock ()
1150+ defer optr .featureGatesMutex .RUnlock ()
1151+
1152+ // Return a copy to prevent external modification
1153+ return optr .enabledManifestFeatureGates .Clone ()
1154+ }
1155+
1156+ // extractEnabledGates extracts the list of enabled feature gates for the current cluster version
1157+ func (optr * Operator ) extractEnabledGates (featureGate * configv1.FeatureGate ) sets.Set [string ] {
1158+ // Find the feature gate details for the current loaded payload version.
1159+ currentVersion := optr .currentVersion ().Version
1160+ if currentVersion == "" {
1161+ klog .Warningf ("Payload has not been initialized yet, using the operator version %s" , optr .enabledCVOFeatureGates .DesiredVersion ())
1162+ currentVersion = optr .enabledFeatureGates .DesiredVersion ()
1163+ }
1164+
1165+ return featuregates .ExtractEnabledGates (featureGate , currentVersion )
1166+ }
1167+
10871168// shouldReconcileCVOConfiguration returns whether the CVO should reconcile its configuration using the API server.
10881169//
10891170// enabledFeatureGates must be initialized before the function is called.
0 commit comments