Skip to content

Commit bdd3553

Browse files
Merge pull request #1273 from JoelSpeed/feature-gate-inclusion
OCPSTRAT-2485: Introduce feature gate based inclusion/exclusion of manifests
2 parents 557510e + f09b811 commit bdd3553

31 files changed

Lines changed: 1451 additions & 276 deletions

bootstrap/bootstrap-pod.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ metadata:
55
namespace: openshift-cluster-version
66
labels:
77
k8s-app: cluster-version-operator
8+
annotations:
9+
include.release.openshift.io/{{ .ClusterProfile }}: "true"
810
spec:
911
containers:
1012
- name: cluster-version-operator

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ require (
1313
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250220212757-b9c4d98a0c45
1414
github.com/openshift/api v0.0.0-20260116192047-6fb7fdae95fd
1515
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13
16-
github.com/openshift/library-go v0.0.0-20260108135436-db8dbd64c462
16+
github.com/openshift/library-go v0.0.0-20260121132910-dc3a1c884c04
1717
github.com/operator-framework/api v0.17.1
1818
github.com/operator-framework/operator-lifecycle-manager v0.22.0
1919
github.com/pkg/errors v0.9.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ github.com/openshift/api v0.0.0-20260116192047-6fb7fdae95fd h1:Hpjq/55Qb0Gy65Rew
9090
github.com/openshift/api v0.0.0-20260116192047-6fb7fdae95fd/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
9191
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13 h1:6rd4zSo2UaWQcAPZfHK9yzKVqH0BnMv1hqMzqXZyTds=
9292
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13/go.mod h1:YvOmPmV7wcJxpfhTDuFqqs2Xpb3M3ovsM6Qs/i2ptq4=
93-
github.com/openshift/library-go v0.0.0-20260108135436-db8dbd64c462 h1:zX9Od4Jg8sVmwQLwk6Vd+BX7tcyC/462FVvDdzHEPPk=
94-
github.com/openshift/library-go v0.0.0-20260108135436-db8dbd64c462/go.mod h1:nIzWQQE49XbiKizVnVOip9CEB7HJ0hoJwNi3g3YKnKc=
93+
github.com/openshift/library-go v0.0.0-20260121132910-dc3a1c884c04 h1:Fm9C8pT4l6VjpdqdhI1cBX9Y3D3S+rFxptVhCPBbMAI=
94+
github.com/openshift/library-go v0.0.0-20260121132910-dc3a1c884c04/go.mod h1:nIzWQQE49XbiKizVnVOip9CEB7HJ0hoJwNi3g3YKnKc=
9595
github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 h1:AKx/w1qpS8We43bsRgf8Nll3CGlDHpr/WAXvuedTNZI=
9696
github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
9797
github.com/operator-framework/api v0.17.1 h1:J/6+Xj4IEV8C7hcirqUFwOiZAU3PbnJhWvB0/bB51c4=

hack/cluster-version-util/task_graph.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/spf13/cobra"
9+
"k8s.io/apimachinery/pkg/util/sets"
910

1011
"github.com/openshift/cluster-version-operator/pkg/payload"
1112
)
@@ -30,7 +31,7 @@ func newTaskGraphCmd() *cobra.Command {
3031

3132
func runTaskGraphCmd(cmd *cobra.Command, args []string) error {
3233
manifestDir := args[0]
33-
release, err := payload.LoadUpdate(manifestDir, "", "", "", payload.DefaultClusterProfile, nil)
34+
release, err := payload.LoadUpdate(manifestDir, "", "", "", payload.DefaultClusterProfile, nil, sets.Set[string]{})
3435
if err != nil {
3536
return err
3637
}

lib/manifest/manifest.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type InclusionConfiguration struct {
3333

3434
// Platform, if non-nil, excludes CredentialsRequests manifests unless they match the infrastructure platform.
3535
Platform *string
36+
37+
// EnabledFeatureGates excludes manifests with a feature gate requirement when the condition is not met.
38+
EnabledFeatureGates sets.Set[string]
3639
}
3740

3841
// GetImplicitlyEnabledCapabilities returns a set of capabilities that are implicitly enabled after a cluster update.
@@ -57,6 +60,7 @@ func GetImplicitlyEnabledCapabilities(
5760
manifestInclusionConfiguration.Profile,
5861
manifestInclusionConfiguration.Capabilities,
5962
manifestInclusionConfiguration.Overrides,
63+
manifestInclusionConfiguration.EnabledFeatureGates,
6064
true,
6165
)
6266
// update manifest is enabled, no need to check
@@ -74,6 +78,7 @@ func GetImplicitlyEnabledCapabilities(
7478
manifestInclusionConfiguration.Profile,
7579
manifestInclusionConfiguration.Capabilities,
7680
manifestInclusionConfiguration.Overrides,
81+
manifestInclusionConfiguration.EnabledFeatureGates,
7782
true,
7883
); err != nil {
7984
continue

pkg/cvo/availableupdates_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ func TestSyncAvailableUpdates(t *testing.T) {
236236
}
237237
expectedAvailableUpdates.RiskConditions = map[string][]metav1.Condition{"FourFiveSix": {{Type: "Applies", Status: "True", Reason: "Match"}}}
238238

239-
optr.enabledFeatureGates = featuregates.DefaultCvoGates("version")
239+
optr.enabledCVOFeatureGates = featuregates.DefaultCvoGates("version")
240240
err := optr.syncAvailableUpdates(context.Background(), cvFixture)
241241

242242
if err != nil {
@@ -325,7 +325,7 @@ func TestSyncAvailableUpdates_ConditionalUpdateRecommendedConditions(t *testing.
325325
tc.modifyOriginalState(optr)
326326
tc.modifyCV(cv, fixture.expectedConditionalUpdates[0])
327327

328-
optr.enabledFeatureGates = featuregates.DefaultCvoGates("version")
328+
optr.enabledCVOFeatureGates = featuregates.DefaultCvoGates("version")
329329
err := optr.syncAvailableUpdates(context.Background(), cv)
330330

331331
if err != nil {
@@ -820,7 +820,7 @@ func TestSyncAvailableUpdatesDesiredUpdate(t *testing.T) {
820820

821821
cv := cvFixture.DeepCopy()
822822
cv.Spec.DesiredUpdate = tt.args.desiredUpdate
823-
optr.enabledFeatureGates = featuregates.DefaultCvoGates("version")
823+
optr.enabledCVOFeatureGates = featuregates.DefaultCvoGates("version")
824824
if err := optr.syncAvailableUpdates(context.Background(), cv); err != nil {
825825
t.Fatalf("syncAvailableUpdates() unexpected error: %v", err)
826826
}

pkg/cvo/cvo.go

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
10901174
func (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.
10991183
func (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

Comments
 (0)