@@ -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.
@@ -178,16 +180,23 @@ type Operator struct {
178180 // to select the manifests that will be applied in the cluster. The starting value cannot be changed in the executing
179181 // CVO but the featurechangestopper controller will detect a feature set change in the cluster and shutdown the CVO.
180182 // Enforcing featuresets is a standard GA CVO behavior that supports the feature gating functionality across the whole
181- // cluster, as opposed to the enabledFeatureGates which controls what gated behaviors of CVO itself are enabled by
183+ // cluster, as opposed to the enabledCVOFeatureGates which controls what gated behaviors of CVO itself are enabled by
182184 // the cluster feature gates.
183185 // See: https://github.com/openshift/enhancements/blob/master/enhancements/update/cvo-techpreview-manifests.md
184186 requiredFeatureSet configv1.FeatureSet
185187
186- // enabledFeatureGates is the checker for what gated CVO behaviors are enabled or disabled by specific cluster-level
188+ // enabledCVOFeatureGates is the checker for what gated CVO behaviors are enabled or disabled by specific cluster-level
187189 // feature gates. It allows multiplexing the cluster-level feature gates to more granular CVO-level gates. Similarly
188- // to the requiredFeatureSet, the enabledFeatureGates cannot be changed in the executing CVO but the
190+ // to the requiredFeatureSet, the enabledCVOFeatureGates cannot be changed in the executing CVO but the
189191 // featurechangestopper controller will detect when cluster feature gate config changes and shutdown the CVO.
190- enabledFeatureGates featuregates.CvoGateChecker
192+ enabledCVOFeatureGates featuregates.CvoGateChecker
193+
194+ // featureGatesMutex protects access to enabledManifestFeatureGates
195+ featureGatesMutex sync.RWMutex
196+ // enabledManifestFeatureGates is the set of feature gates that are currently enabled for the manifests that are applied to the cluster.
197+ // This is the full set of enabled feature gates extracted from the FeatureGate object.
198+ // We use this set as a filter to determine which of the manifests from the payload should or should not be applied to the cluster.
199+ enabledManifestFeatureGates sets.Set [string ]
191200
192201 clusterProfile string
193202 uid types.UID
@@ -213,6 +222,7 @@ func New(
213222 cmConfigManagedInformer informerscorev1.ConfigMapInformer ,
214223 proxyInformer configinformersv1.ProxyInformer ,
215224 operatorInformerFactory operatorexternalversions.SharedInformerFactory ,
225+ featureGateInformer configinformersv1.FeatureGateInformer ,
216226 client clientset.Interface ,
217227 kubeClient kubernetes.Interface ,
218228 operatorClient operatorclientset.Interface ,
@@ -225,6 +235,7 @@ func New(
225235 alwaysEnableCapabilities []configv1.ClusterVersionCapability ,
226236 featureSet configv1.FeatureSet ,
227237 cvoGates featuregates.CvoGateChecker ,
238+ startingEnabledManifestFeatureGates sets.Set [string ],
228239) (* Operator , error ) {
229240 eventBroadcaster := record .NewBroadcaster ()
230241 eventBroadcaster .StartLogging (klog .Infof )
@@ -248,18 +259,19 @@ func New(
248259 kubeClient : kubeClient ,
249260 operatorClient : operatorClient ,
250261 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" }),
262+ queue : workqueue .NewTypedRateLimitingQueueWithConfig (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "clusterversion" }),
263+ availableUpdatesQueue : workqueue .NewTypedRateLimitingQueueWithConfig (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "availableupdates" }),
264+ upgradeableQueue : workqueue .NewTypedRateLimitingQueueWithConfig (workqueue .DefaultTypedControllerRateLimiter [any ](), workqueue.TypedRateLimitingQueueConfig [any ]{Name : "upgradeable" }),
254265
255266 hypershift : hypershift ,
256267 exclude : exclude ,
257268 clusterProfile : clusterProfile ,
258269 conditionRegistry : standard .NewConditionRegistry (promqlTarget ),
259270 injectClusterIdIntoPromQL : injectClusterIdIntoPromQL ,
260271
261- requiredFeatureSet : featureSet ,
262- enabledFeatureGates : cvoGates ,
272+ requiredFeatureSet : featureSet ,
273+ enabledCVOFeatureGates : cvoGates ,
274+ enabledManifestFeatureGates : startingEnabledManifestFeatureGates ,
263275
264276 alwaysEnableCapabilities : alwaysEnableCapabilities ,
265277 }
@@ -276,6 +288,9 @@ func New(
276288 if _ , err := coInformer .Informer ().AddEventHandler (optr .clusterOperatorEventHandler ()); err != nil {
277289 return nil , err
278290 }
291+ if _ , err := featureGateInformer .Informer ().AddEventHandler (optr .featureGateEventHandler ()); err != nil {
292+ return nil , err
293+ }
279294
280295 optr .coLister = coInformer .Lister ()
281296 optr .cacheSynced = append (optr .cacheSynced , coInformer .Informer ().HasSynced )
@@ -287,6 +302,9 @@ func New(
287302 optr .cmConfigLister = cmConfigInformer .Lister ().ConfigMaps (internal .ConfigNamespace )
288303 optr .cmConfigManagedLister = cmConfigManagedInformer .Lister ().ConfigMaps (internal .ConfigManagedNamespace )
289304
305+ optr .featureGateLister = featureGateInformer .Lister ()
306+ optr .cacheSynced = append (optr .cacheSynced , featureGateInformer .Informer ().HasSynced )
307+
290308 // make sure this is initialized after all the listers are initialized
291309 optr .upgradeableChecks = optr .defaultUpgradeableChecks ()
292310
@@ -318,7 +336,7 @@ func (optr *Operator) LoadInitialPayload(ctx context.Context, restConfig *rest.C
318336 }
319337
320338 update , err := payload .LoadUpdate (optr .defaultPayloadDir (), optr .release .Image , optr .exclude , string (optr .requiredFeatureSet ),
321- optr .clusterProfile , configv1 .KnownClusterVersionCapabilities )
339+ optr .clusterProfile , configv1 .KnownClusterVersionCapabilities , optr . getEnabledFeatureGates () )
322340
323341 if err != nil {
324342 return nil , fmt .Errorf ("the local release contents are invalid - no current version can be determined from disk: %v" , err )
@@ -779,7 +797,7 @@ func (optr *Operator) sync(ctx context.Context, key string) error {
779797 }
780798
781799 // inform the config sync loop about our desired state
782- status := optr .configSync .Update (ctx , config .Generation , desired , config , state )
800+ status := optr .configSync .Update (ctx , config .Generation , desired , config , state , optr . getEnabledFeatureGates () )
783801
784802 // write cluster version status
785803 return optr .syncStatus (ctx , original , config , status , errs )
@@ -1084,12 +1102,78 @@ func (optr *Operator) HTTPClient() (*http.Client, error) {
10841102 }, nil
10851103}
10861104
1105+ // featureGateEventHandler handles changes to FeatureGate objects and updates the cluster feature gates
1106+ func (optr * Operator ) featureGateEventHandler () cache.ResourceEventHandler {
1107+ workQueueKey := optr .queueKey ()
1108+ return cache.ResourceEventHandlerFuncs {
1109+ AddFunc : func (obj interface {}) {
1110+ if optr .updateEnabledFeatureGates (obj ) {
1111+ optr .queue .Add (workQueueKey )
1112+ }
1113+ },
1114+ UpdateFunc : func (old , new interface {}) {
1115+ if optr .updateEnabledFeatureGates (new ) {
1116+ optr .queue .Add (workQueueKey )
1117+ }
1118+ },
1119+ }
1120+ }
1121+
1122+ // updateEnabledFeatureGates updates the cluster feature gates based on a FeatureGate object.
1123+ // Returns true or false based on whether or not the gates were actually updated.
1124+ // This allows us to avoid unnecessary work if the gates have not changed.
1125+ func (optr * Operator ) updateEnabledFeatureGates (obj interface {}) bool {
1126+ featureGate , ok := obj .(* configv1.FeatureGate )
1127+ if ! ok {
1128+ klog .Warningf ("Expected FeatureGate object but got %T" , obj )
1129+ return false
1130+ }
1131+
1132+ newGates := optr .extractEnabledGates (featureGate )
1133+
1134+ optr .featureGatesMutex .Lock ()
1135+ defer optr .featureGatesMutex .Unlock ()
1136+
1137+ // Check if gates actually changed to avoid unnecessary work
1138+ if ! optr .enabledManifestFeatureGates .Equal (newGates ) {
1139+
1140+ klog .V (2 ).Infof ("Cluster feature gates changed from %v to %v" ,
1141+ sets .List (optr .enabledManifestFeatureGates ), sets .List (newGates ))
1142+
1143+ optr .enabledManifestFeatureGates = newGates
1144+ return true
1145+ }
1146+
1147+ return false
1148+ }
1149+
1150+ // getEnabledFeatureGates returns a copy of the current cluster feature gates for safe consumption
1151+ func (optr * Operator ) getEnabledFeatureGates () sets.Set [string ] {
1152+ optr .featureGatesMutex .RLock ()
1153+ defer optr .featureGatesMutex .RUnlock ()
1154+
1155+ // Return a copy to prevent external modification
1156+ return optr .enabledManifestFeatureGates .Clone ()
1157+ }
1158+
1159+ // extractEnabledGates extracts the list of enabled feature gates for the current cluster version
1160+ func (optr * Operator ) extractEnabledGates (featureGate * configv1.FeatureGate ) sets.Set [string ] {
1161+ // Find the feature gate details for the current loaded payload version.
1162+ currentVersion := optr .currentVersion ().Version
1163+ if currentVersion == "" {
1164+ klog .Warningf ("Payload has not been initialized yet, using the operator version %s" , optr .enabledCVOFeatureGates .DesiredVersion ())
1165+ currentVersion = optr .enabledCVOFeatureGates .DesiredVersion ()
1166+ }
1167+
1168+ return featuregates .ExtractEnabledGates (featureGate , currentVersion )
1169+ }
1170+
10871171// shouldReconcileCVOConfiguration returns whether the CVO should reconcile its configuration using the API server.
10881172//
1089- // enabledFeatureGates must be initialized before the function is called.
1173+ // enabledCVOFeatureGates must be initialized before the function is called.
10901174func (optr * Operator ) shouldReconcileCVOConfiguration () bool {
10911175 // The relevant CRD and CR are not applied in HyperShift, which configures the CVO via a configuration file
1092- return optr .enabledFeatureGates .CVOConfiguration () && ! optr .hypershift
1176+ return optr .enabledCVOFeatureGates .CVOConfiguration () && ! optr .hypershift
10931177}
10941178
10951179// shouldReconcileAcceptRisks returns whether the CVO should reconcile spec.desiredUpdate.acceptRisks and populate the
@@ -1098,5 +1182,5 @@ func (optr *Operator) shouldReconcileCVOConfiguration() bool {
10981182// enabledFeatureGates must be initialized before the function is called.
10991183func (optr * Operator ) shouldReconcileAcceptRisks () bool {
11001184 // HyperShift will be supported later if needed
1101- return optr .enabledFeatureGates .AcceptRisks () && ! optr .hypershift
1185+ return optr .enabledCVOFeatureGates .AcceptRisks () && ! optr .hypershift
11021186}
0 commit comments