Skip to content

Commit 226437e

Browse files
committed
Pass feature gating information through to sync workers
1 parent 37b1cd3 commit 226437e

17 files changed

Lines changed: 766 additions & 68 deletions

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/cvo.go

Lines changed: 88 additions & 7 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.
@@ -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.

pkg/cvo/cvo_featuregates_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package cvo
2+
3+
import (
4+
"testing"
5+
6+
configv1 "github.com/openshift/api/config/v1"
7+
"k8s.io/apimachinery/pkg/util/sets"
8+
)
9+
10+
func TestOperator_extractEnabledGates(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
featureGate *configv1.FeatureGate
14+
release configv1.Release
15+
expected sets.Set[string]
16+
}{
17+
{
18+
name: "extract gates for matching version",
19+
featureGate: &configv1.FeatureGate{
20+
Status: configv1.FeatureGateStatus{
21+
FeatureGates: []configv1.FeatureGateDetails{
22+
{
23+
Version: "4.14.0",
24+
Enabled: []configv1.FeatureGateAttributes{
25+
{Name: "TechPreviewFeatureGate"},
26+
{Name: "ExperimentalFeature"},
27+
},
28+
},
29+
{
30+
Version: "4.13.0",
31+
Enabled: []configv1.FeatureGateAttributes{
32+
{Name: "OldFeature"},
33+
},
34+
},
35+
},
36+
},
37+
},
38+
release: configv1.Release{Version: "4.14.0"},
39+
expected: sets.New[string]("TechPreviewFeatureGate", "ExperimentalFeature"),
40+
},
41+
{
42+
name: "no matching version - return empty",
43+
featureGate: &configv1.FeatureGate{
44+
Status: configv1.FeatureGateStatus{
45+
FeatureGates: []configv1.FeatureGateDetails{
46+
{
47+
Version: "4.13.0",
48+
Enabled: []configv1.FeatureGateAttributes{
49+
{Name: "OldFeature"},
50+
},
51+
},
52+
},
53+
},
54+
},
55+
release: configv1.Release{Version: "4.14.0"},
56+
expected: sets.Set[string]{},
57+
},
58+
{
59+
name: "empty enabled gates",
60+
featureGate: &configv1.FeatureGate{
61+
Status: configv1.FeatureGateStatus{
62+
FeatureGates: []configv1.FeatureGateDetails{
63+
{
64+
Version: "4.14.0",
65+
Enabled: []configv1.FeatureGateAttributes{},
66+
},
67+
},
68+
},
69+
},
70+
release: configv1.Release{Version: "4.14.0"},
71+
expected: sets.Set[string]{},
72+
},
73+
}
74+
75+
for _, tt := range tests {
76+
t.Run(tt.name, func(t *testing.T) {
77+
optr := &Operator{
78+
release: tt.release,
79+
enabledFeatureGates: fakeRiFlags{
80+
desiredVersion: tt.release.Version,
81+
},
82+
}
83+
84+
result := optr.extractEnabledGates(tt.featureGate)
85+
86+
if !result.Equal(tt.expected) {
87+
t.Errorf("extractEnabledGates() = %v, expected %v", sets.List(result), sets.List(tt.expected))
88+
}
89+
})
90+
}
91+
}
92+
93+
func TestOperator_getEnabledFeatureGates(t *testing.T) {
94+
optr := &Operator{
95+
enabledManifestFeatureGates: sets.New[string]("gate1", "gate2"),
96+
}
97+
98+
result := optr.getEnabledFeatureGates()
99+
expected := sets.New[string]("gate1", "gate2")
100+
101+
if !result.Equal(expected) {
102+
t.Errorf("getEnabledFeatureGates() = %v, expected %v", sets.List(result), sets.List(expected))
103+
}
104+
105+
// Verify it returns a copy by modifying the result
106+
result.Insert("gate3")
107+
result2 := optr.getEnabledFeatureGates()
108+
109+
if result2.Has("gate3") {
110+
t.Error("getEnabledFeatureGates() should return a copy, but original was modified")
111+
}
112+
}
113+
114+
func TestOperator_updateEnabledFeatureGates(t *testing.T) {
115+
tests := []struct {
116+
name string
117+
obj interface{}
118+
expectUpdate bool
119+
}{
120+
{
121+
name: "valid FeatureGate object",
122+
obj: &configv1.FeatureGate{
123+
Status: configv1.FeatureGateStatus{
124+
FeatureGates: []configv1.FeatureGateDetails{
125+
{
126+
Version: "4.14.0",
127+
Enabled: []configv1.FeatureGateAttributes{
128+
{Name: "NewGate"},
129+
},
130+
},
131+
},
132+
},
133+
},
134+
expectUpdate: true,
135+
},
136+
{
137+
name: "invalid object type",
138+
obj: "not-a-feature-gate",
139+
expectUpdate: false,
140+
},
141+
{
142+
name: "nil object",
143+
obj: nil,
144+
expectUpdate: false,
145+
},
146+
}
147+
148+
for _, tt := range tests {
149+
t.Run(tt.name, func(t *testing.T) {
150+
optr := &Operator{
151+
enabledManifestFeatureGates: sets.New[string]("oldgate"),
152+
release: configv1.Release{Version: "4.14.0"},
153+
enabledFeatureGates: fakeRiFlags{
154+
desiredVersion: "4.14.0",
155+
},
156+
}
157+
158+
originalGates := optr.getEnabledFeatureGates()
159+
optr.updateEnabledFeatureGates(tt.obj)
160+
newGates := optr.getEnabledFeatureGates()
161+
162+
if tt.expectUpdate {
163+
if newGates.Equal(originalGates) {
164+
t.Error("updateEnabledFeatureGates() expected gates to be updated")
165+
}
166+
if !newGates.Has("NewGate") {
167+
t.Error("updateEnabledFeatureGates() expected NewGate to be enabled")
168+
}
169+
} else {
170+
if !newGates.Equal(originalGates) {
171+
t.Error("updateEnabledFeatureGates() should not update gates for invalid object")
172+
}
173+
}
174+
})
175+
}
176+
}

0 commit comments

Comments
 (0)