Skip to content

Commit 7329a7e

Browse files
committed
Make the CVO feature-gate-aware when populating status.capabilities (filter out capabilities whose feature gates aren't enabled)
1 parent 09b1000 commit 7329a7e

4 files changed

Lines changed: 217 additions & 39 deletions

File tree

lib/capability/capability.go

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,45 @@ import (
99
configv1 "github.com/openshift/api/config/v1"
1010
)
1111

12+
// featureGatedCapabilities maps feature gate names to the capabilities they
13+
// gate. When a feature gate is absent from the enabled set, its capabilities
14+
// are excluded from Known, Enabled, and baseline capability set resolution.
15+
// This mirrors the FeatureGateAwareEnum annotations on ClusterVersionCapability
16+
// in the openshift/api types.
17+
var featureGatedCapabilities = map[string][]configv1.ClusterVersionCapability{
18+
"CRDCompatibilityRequirementOperator": {
19+
configv1.ClusterVersionCapabilityCompatibilityRequirements,
20+
configv1.ClusterVersionCapabilityClusterAPI,
21+
},
22+
}
23+
24+
// gatedCapabilities returns the set of capabilities that should be excluded
25+
// because their required feature gates are not in enabledFeatureGates.
26+
func gatedCapabilities(enabledFeatureGates sets.Set[string]) sets.Set[configv1.ClusterVersionCapability] {
27+
excluded := sets.New[configv1.ClusterVersionCapability]()
28+
for gate, caps := range featureGatedCapabilities {
29+
if !enabledFeatureGates.Has(gate) {
30+
excluded.Insert(caps...)
31+
}
32+
}
33+
return excluded
34+
}
35+
36+
// FilterByFeatureGates returns the subset of capabilities whose required
37+
// feature gates are enabled.
38+
func FilterByFeatureGates(capabilities []configv1.ClusterVersionCapability,
39+
enabledFeatureGates sets.Set[string]) []configv1.ClusterVersionCapability {
40+
41+
excluded := gatedCapabilities(enabledFeatureGates)
42+
var filtered []configv1.ClusterVersionCapability
43+
for _, c := range capabilities {
44+
if !excluded.Has(c) {
45+
filtered = append(filtered, c)
46+
}
47+
}
48+
return filtered
49+
}
50+
1251
type ClusterCapabilities struct {
1352
Known sets.Set[configv1.ClusterVersionCapability]
1453
Enabled sets.Set[configv1.ClusterVersionCapability]
@@ -35,15 +74,19 @@ func SortedList(s sets.Set[configv1.ClusterVersionCapability]) []configv1.Cluste
3574
// SetCapabilities populates and returns cluster capabilities from ClusterVersion's capabilities specification and a
3675
// collection of capabilities that are enabled including implicitly enabled.
3776
// It ensures that each capability in the collection is still enabled in the returned cluster capabilities
38-
// and recognizes all implicitly enabled ones.
77+
// and recognizes all implicitly enabled ones. Capabilities gated behind feature gates that are not in
78+
// enabledFeatureGates are excluded from both Known and Enabled sets.
3979
func SetCapabilities(config *configv1.ClusterVersion,
40-
capabilities []configv1.ClusterVersionCapability) ClusterCapabilities {
80+
capabilities []configv1.ClusterVersionCapability,
81+
enabledFeatureGates sets.Set[string]) ClusterCapabilities {
82+
83+
excluded := gatedCapabilities(enabledFeatureGates)
4184

4285
var clusterCapabilities ClusterCapabilities
43-
clusterCapabilities.Known = sets.New[configv1.ClusterVersionCapability](configv1.KnownClusterVersionCapabilities...)
86+
clusterCapabilities.Known = sets.New[configv1.ClusterVersionCapability](configv1.KnownClusterVersionCapabilities...).Difference(excluded)
4487

4588
clusterCapabilities.Enabled, clusterCapabilities.ImplicitlyEnabled =
46-
categorizeEnabledCapabilities(config.Spec.Capabilities, capabilities)
89+
categorizeEnabledCapabilities(config.Spec.Capabilities, capabilities, excluded)
4790

4891
return clusterCapabilities
4992
}
@@ -87,22 +130,31 @@ func GetCapabilitiesStatus(capabilities ClusterCapabilities) configv1.ClusterVer
87130

88131
// categorizeEnabledCapabilities categorizes enabled capabilities by implicitness from cluster version's
89132
// capabilities specification and a collection of capabilities that are enabled including implicitly enabled.
133+
// Capabilities in the excluded set are filtered from all sources (baseline set, additional, and prior enabled).
90134
func categorizeEnabledCapabilities(capabilitiesSpec *configv1.ClusterVersionCapabilitiesSpec,
91-
capabilities []configv1.ClusterVersionCapability) (sets.Set[configv1.ClusterVersionCapability],
135+
capabilities []configv1.ClusterVersionCapability,
136+
excluded sets.Set[configv1.ClusterVersionCapability]) (sets.Set[configv1.ClusterVersionCapability],
92137
sets.Set[configv1.ClusterVersionCapability]) {
93138

94139
capSet := configv1.ClusterVersionCapabilitySetCurrent
95140

96141
if capabilitiesSpec != nil && len(capabilitiesSpec.BaselineCapabilitySet) > 0 {
97142
capSet = capabilitiesSpec.BaselineCapabilitySet
98143
}
99-
enabled := sets.New[configv1.ClusterVersionCapability](configv1.ClusterVersionCapabilitySets[capSet]...)
144+
enabled := sets.New[configv1.ClusterVersionCapability](configv1.ClusterVersionCapabilitySets[capSet]...).Difference(excluded)
100145

101146
if capabilitiesSpec != nil {
102-
enabled.Insert(capabilitiesSpec.AdditionalEnabledCapabilities...)
147+
for _, c := range capabilitiesSpec.AdditionalEnabledCapabilities {
148+
if !excluded.Has(c) {
149+
enabled.Insert(c)
150+
}
151+
}
103152
}
104153
implicitlyEnabled := sets.New[configv1.ClusterVersionCapability]()
105154
for _, k := range capabilities {
155+
if excluded.Has(k) {
156+
continue
157+
}
106158
if !enabled.Has(k) {
107159
implicitlyEnabled.Insert(k)
108160
enabled.Insert(k)

lib/capability/capability_test.go

Lines changed: 144 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,54 @@ import (
1111
configv1 "github.com/openshift/api/config/v1"
1212
)
1313

14+
// allFeatureGates returns a feature gate set with all gates that gate capabilities enabled.
15+
func allFeatureGates() sets.Set[string] {
16+
gates := sets.New[string]()
17+
for gate := range featureGatedCapabilities {
18+
gates.Insert(gate)
19+
}
20+
return gates
21+
}
22+
1423
func TestSetCapabilities(t *testing.T) {
1524
tests := []struct {
16-
name string
17-
config *configv1.ClusterVersion
18-
wantKnownKeys []configv1.ClusterVersionCapability
19-
wantEnabledKeys []configv1.ClusterVersionCapability
25+
name string
26+
config *configv1.ClusterVersion
27+
enabledGates sets.Set[string]
28+
wantKnownKeys []configv1.ClusterVersionCapability
29+
wantEnabledKeys []configv1.ClusterVersionCapability
30+
setDefaultKnown bool
31+
setDefaultEnabled bool
2032
}{
21-
{name: "capabilities nil",
22-
config: &configv1.ClusterVersion{},
23-
// wantKnownKeys and wantEnabledKeys will be set to default set of capabilities by test
33+
{name: "capabilities nil, all gates enabled",
34+
config: &configv1.ClusterVersion{},
35+
enabledGates: allFeatureGates(),
36+
setDefaultKnown: true,
37+
setDefaultEnabled: true,
2438
},
25-
{name: "capabilities set not set",
39+
{name: "capabilities set not set, all gates enabled",
2640
config: &configv1.ClusterVersion{
2741
Spec: configv1.ClusterVersionSpec{
2842
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{},
2943
},
3044
},
31-
// wantKnownKeys and wantEnabledKeys will be set to default set of capabilities by test
45+
enabledGates: allFeatureGates(),
46+
setDefaultKnown: true,
47+
setDefaultEnabled: true,
3248
},
33-
{name: "set capabilities None",
49+
{name: "set capabilities None, all gates enabled",
3450
config: &configv1.ClusterVersion{
3551
Spec: configv1.ClusterVersionSpec{
3652
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
3753
BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetNone,
3854
},
3955
},
4056
},
57+
enabledGates: allFeatureGates(),
4158
wantKnownKeys: configv1.KnownClusterVersionCapabilities,
4259
wantEnabledKeys: []configv1.ClusterVersionCapability{},
4360
},
44-
{name: "set capabilities 4_11",
61+
{name: "set capabilities 4_11, all gates enabled",
4562
config: &configv1.ClusterVersion{
4663
Spec: configv1.ClusterVersionSpec{
4764
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
@@ -50,6 +67,7 @@ func TestSetCapabilities(t *testing.T) {
5067
},
5168
},
5269
},
70+
enabledGates: allFeatureGates(),
5371
wantKnownKeys: configv1.KnownClusterVersionCapabilities,
5472
wantEnabledKeys: []configv1.ClusterVersionCapability{
5573
configv1.ClusterVersionCapabilityBaremetal,
@@ -58,7 +76,7 @@ func TestSetCapabilities(t *testing.T) {
5876
configv1.ClusterVersionCapabilityMachineAPI,
5977
},
6078
},
61-
{name: "set capabilities vCurrent",
79+
{name: "set capabilities vCurrent, all gates enabled",
6280
config: &configv1.ClusterVersion{
6381
Spec: configv1.ClusterVersionSpec{
6482
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
@@ -67,10 +85,11 @@ func TestSetCapabilities(t *testing.T) {
6785
},
6886
},
6987
},
88+
enabledGates: allFeatureGates(),
7089
wantKnownKeys: configv1.KnownClusterVersionCapabilities,
7190
wantEnabledKeys: configv1.ClusterVersionCapabilitySets[configv1.ClusterVersionCapabilitySetCurrent],
7291
},
73-
{name: "set capabilities None with additional",
92+
{name: "set capabilities None with additional, all gates enabled",
7493
config: &configv1.ClusterVersion{
7594
Spec: configv1.ClusterVersionSpec{
7695
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
@@ -79,10 +98,11 @@ func TestSetCapabilities(t *testing.T) {
7998
},
8099
},
81100
},
101+
enabledGates: allFeatureGates(),
82102
wantKnownKeys: configv1.KnownClusterVersionCapabilities,
83103
wantEnabledKeys: []configv1.ClusterVersionCapability{"cap1", "cap2", "cap3"},
84104
},
85-
{name: "set capabilities 4_11 with additional",
105+
{name: "set capabilities 4_11 with additional, all gates enabled",
86106
config: &configv1.ClusterVersion{
87107
Spec: configv1.ClusterVersionSpec{
88108
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
@@ -91,6 +111,7 @@ func TestSetCapabilities(t *testing.T) {
91111
},
92112
},
93113
},
114+
enabledGates: allFeatureGates(),
94115
wantKnownKeys: configv1.KnownClusterVersionCapabilities,
95116
wantEnabledKeys: []configv1.ClusterVersionCapability{
96117
configv1.ClusterVersionCapabilityBaremetal,
@@ -102,32 +123,87 @@ func TestSetCapabilities(t *testing.T) {
102123
"cap3",
103124
},
104125
},
126+
{name: "capabilities nil, no gates enabled",
127+
config: &configv1.ClusterVersion{},
128+
enabledGates: sets.New[string](),
129+
wantKnownKeys: FilterByFeatureGates(
130+
configv1.KnownClusterVersionCapabilities, sets.New[string]()),
131+
wantEnabledKeys: FilterByFeatureGates(
132+
configv1.ClusterVersionCapabilitySets[configv1.ClusterVersionCapabilitySetCurrent], sets.New[string]()),
133+
},
134+
{name: "set capabilities vCurrent, no gates enabled",
135+
config: &configv1.ClusterVersion{
136+
Spec: configv1.ClusterVersionSpec{
137+
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
138+
BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetCurrent,
139+
AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{},
140+
},
141+
},
142+
},
143+
enabledGates: sets.New[string](),
144+
wantKnownKeys: FilterByFeatureGates(
145+
configv1.KnownClusterVersionCapabilities, sets.New[string]()),
146+
wantEnabledKeys: FilterByFeatureGates(
147+
configv1.ClusterVersionCapabilitySets[configv1.ClusterVersionCapabilitySetCurrent], sets.New[string]()),
148+
},
149+
{name: "gated capability in additional, gate disabled",
150+
config: &configv1.ClusterVersion{
151+
Spec: configv1.ClusterVersionSpec{
152+
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
153+
BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetNone,
154+
AdditionalEnabledCapabilities: []configv1.ClusterVersionCapability{
155+
configv1.ClusterVersionCapabilityBaremetal,
156+
configv1.ClusterVersionCapabilityClusterAPI,
157+
},
158+
},
159+
},
160+
},
161+
enabledGates: sets.New[string](),
162+
wantKnownKeys: FilterByFeatureGates(
163+
configv1.KnownClusterVersionCapabilities, sets.New[string]()),
164+
wantEnabledKeys: []configv1.ClusterVersionCapability{
165+
configv1.ClusterVersionCapabilityBaremetal,
166+
},
167+
},
105168
}
106169
for _, test := range tests {
107170
t.Run(test.name, func(t *testing.T) {
108-
caps := SetCapabilities(test.config, nil)
109-
if test.config.Spec.Capabilities == nil || (test.config.Spec.Capabilities != nil &&
110-
len(test.config.Spec.Capabilities.BaselineCapabilitySet) == 0) {
111-
112-
test.wantKnownKeys = configv1.KnownClusterVersionCapabilities
113-
test.wantEnabledKeys = configv1.ClusterVersionCapabilitySets[configv1.ClusterVersionCapabilitySetCurrent]
171+
caps := SetCapabilities(test.config, nil, test.enabledGates)
172+
wantKnown := test.wantKnownKeys
173+
wantEnabled := test.wantEnabledKeys
174+
if test.setDefaultKnown {
175+
wantKnown = configv1.KnownClusterVersionCapabilities
176+
}
177+
if test.setDefaultEnabled {
178+
wantEnabled = configv1.ClusterVersionCapabilitySets[configv1.ClusterVersionCapabilitySetCurrent]
114179
}
115-
if len(caps.Known) != len(test.wantKnownKeys) {
116-
t.Errorf("Incorrect number of Known keys, wanted: %q. Known returned: %v", test.wantKnownKeys, caps.Known)
180+
if len(caps.Known) != len(wantKnown) {
181+
t.Errorf("Incorrect number of Known keys, wanted: %q. Known returned: %v", wantKnown, caps.Known)
117182
}
118-
for _, v := range test.wantKnownKeys {
183+
for _, v := range wantKnown {
119184
if _, ok := caps.Known[v]; !ok {
120185
t.Errorf("Missing Known key %q. Known returned : %v", v, caps.Known)
121186
}
122187
}
123-
if len(caps.Enabled) != len(test.wantEnabledKeys) {
124-
t.Errorf("Incorrect number of Enabled keys, wanted: %q. Enabled returned: %v", test.wantEnabledKeys, caps.Enabled)
188+
if len(caps.Enabled) != len(wantEnabled) {
189+
t.Errorf("Incorrect number of Enabled keys, wanted: %q. Enabled returned: %v", wantEnabled, caps.Enabled)
125190
}
126-
for _, v := range test.wantEnabledKeys {
191+
for _, v := range wantEnabled {
127192
if _, ok := caps.Enabled[v]; !ok {
128193
t.Errorf("Missing Enabled key %q. Enabled returned : %v", v, caps.Enabled)
129194
}
130195
}
196+
197+
// Verify gated capabilities are absent when gate is disabled
198+
excluded := gatedCapabilities(test.enabledGates)
199+
for cap := range excluded {
200+
if caps.Known.Has(cap) {
201+
t.Errorf("Gated capability %q should not be in Known when gate is disabled", cap)
202+
}
203+
if caps.Enabled.Has(cap) {
204+
t.Errorf("Gated capability %q should not be in Enabled when gate is disabled", cap)
205+
}
206+
}
131207
})
132208
}
133209
}
@@ -136,6 +212,7 @@ func TestSetCapabilitiesWithImplicitlyEnabled(t *testing.T) {
136212
tests := []struct {
137213
name string
138214
config *configv1.ClusterVersion
215+
enabledGates sets.Set[string]
139216
wantImplicit sets.Set[configv1.ClusterVersionCapability]
140217
priorEnabled []configv1.ClusterVersionCapability
141218
}{
@@ -147,13 +224,29 @@ func TestSetCapabilitiesWithImplicitlyEnabled(t *testing.T) {
147224
},
148225
},
149226
},
227+
enabledGates: allFeatureGates(),
150228
priorEnabled: []configv1.ClusterVersionCapability{"cap1", "cap2", "cap3", "cap2"},
151229
wantImplicit: sets.New[configv1.ClusterVersionCapability]("cap1", "cap2", "cap3"),
152230
},
231+
{name: "prior-enabled gated capability with gate disabled",
232+
config: &configv1.ClusterVersion{
233+
Spec: configv1.ClusterVersionSpec{
234+
Capabilities: &configv1.ClusterVersionCapabilitiesSpec{
235+
BaselineCapabilitySet: configv1.ClusterVersionCapabilitySetCurrent,
236+
},
237+
},
238+
},
239+
enabledGates: sets.New[string](),
240+
priorEnabled: []configv1.ClusterVersionCapability{
241+
configv1.ClusterVersionCapabilityClusterAPI,
242+
"cap1",
243+
},
244+
wantImplicit: sets.New[configv1.ClusterVersionCapability]("cap1"),
245+
},
153246
}
154247
for _, test := range tests {
155248
t.Run(test.name, func(t *testing.T) {
156-
caps := SetCapabilities(test.config, test.priorEnabled)
249+
caps := SetCapabilities(test.config, test.priorEnabled, test.enabledGates)
157250
if diff := cmp.Diff(test.wantImplicit, caps.ImplicitlyEnabled); diff != "" {
158251
t.Errorf("%s: wantImplicit differs from expected:\n%s", test.name, diff)
159252
}
@@ -286,3 +379,27 @@ func TestSetFromImplicitlyEnabledCapabilities(t *testing.T) {
286379
})
287380
}
288381
}
382+
383+
func TestFilterByFeatureGates(t *testing.T) {
384+
all := configv1.KnownClusterVersionCapabilities
385+
allGates := allFeatureGates()
386+
387+
// With all gates enabled, nothing is filtered
388+
filtered := FilterByFeatureGates(all, allGates)
389+
if len(filtered) != len(all) {
390+
t.Errorf("expected %d capabilities with all gates, got %d", len(all), len(filtered))
391+
}
392+
393+
// With no gates enabled, gated capabilities are removed
394+
filtered = FilterByFeatureGates(all, sets.New[string]())
395+
excluded := gatedCapabilities(sets.New[string]())
396+
expectedLen := len(all) - excluded.Len()
397+
if len(filtered) != expectedLen {
398+
t.Errorf("expected %d capabilities with no gates, got %d", expectedLen, len(filtered))
399+
}
400+
for _, c := range filtered {
401+
if excluded.Has(c) {
402+
t.Errorf("gated capability %q should have been filtered out", c)
403+
}
404+
}
405+
}

0 commit comments

Comments
 (0)