-
Notifications
You must be signed in to change notification settings - Fork 590
OCPSTRAT-2876: Enable major version segmentation of enabled feature gates #2637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d3181b1
3d59085
ddee269
38472df
cb307e6
32dd9b2
d01edaf
6411446
cdeb235
933f310
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |
| "strings" | ||
|
|
||
| configv1 "github.com/openshift/api/config/v1" | ||
| "k8s.io/apimachinery/pkg/util/sets" | ||
| ) | ||
|
|
||
| // FeatureGateDescription is a golang-only interface used to contains details for a feature gate. | ||
|
|
@@ -45,15 +46,103 @@ var ( | |
| kubernetes = OwningProduct("Kubernetes") | ||
| ) | ||
|
|
||
| type featureGateEnableOption func(s *featureGateStatus) | ||
|
|
||
| type versionOperator string | ||
|
|
||
| var ( | ||
| equal = versionOperator("=") | ||
| greaterThan = versionOperator(">") | ||
| greaterThanOrEqual = versionOperator(">=") | ||
| lessThan = versionOperator("<") | ||
| lessThanOrEqual = versionOperator("<=") | ||
| ) | ||
|
|
||
| func inVersion(version uint64, op versionOperator) featureGateEnableOption { | ||
| return func(s *featureGateStatus) { | ||
| switch op { | ||
| case equal: | ||
| s.version.Insert(version) | ||
| case greaterThan: | ||
| for v := version + 1; v <= maxOpenshiftVersion; v++ { | ||
| s.version.Insert(v) | ||
| } | ||
| case greaterThanOrEqual: | ||
| for v := version; v <= maxOpenshiftVersion; v++ { | ||
| s.version.Insert(v) | ||
| } | ||
| case lessThan: | ||
| for v := minOpenshiftVersion; v < version; v++ { | ||
| s.version.Insert(v) | ||
| } | ||
| case lessThanOrEqual: | ||
| for v := minOpenshiftVersion; v <= version; v++ { | ||
| s.version.Insert(v) | ||
| } | ||
| default: | ||
| panic(fmt.Sprintf("invalid version operator: %s", op)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func inClusterProfile(clusterProfile ClusterProfileName) featureGateEnableOption { | ||
| return func(s *featureGateStatus) { | ||
| s.clusterProfile.Insert(clusterProfile) | ||
| } | ||
| } | ||
|
|
||
| func withFeatureSet(featureSet configv1.FeatureSet) featureGateEnableOption { | ||
| return func(s *featureGateStatus) { | ||
| s.featureSets.Insert(featureSet) | ||
| } | ||
| } | ||
|
|
||
| func inDefault() featureGateEnableOption { | ||
| return withFeatureSet(configv1.Default) | ||
| } | ||
|
Comment on lines
+100
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The OKD test requires that all Default enabled feature gates are also in the OKD feature set - would it make sense to make I'm not strongly opinionated here, but it would save some verbosity in the feature gate definitions and needing to be aware of that nuance when promoting gates. |
||
|
|
||
| func inTechPreviewNoUpgrade() featureGateEnableOption { | ||
| return withFeatureSet(configv1.TechPreviewNoUpgrade) | ||
| } | ||
|
|
||
| func inDevPreviewNoUpgrade() featureGateEnableOption { | ||
| return withFeatureSet(configv1.DevPreviewNoUpgrade) | ||
| } | ||
|
|
||
| func inCustomNoUpgrade() featureGateEnableOption { | ||
| return withFeatureSet(configv1.CustomNoUpgrade) | ||
| } | ||
|
|
||
| func inOKD() featureGateEnableOption { | ||
| return withFeatureSet(configv1.OKD) | ||
| } | ||
|
|
||
| type featureGateBuilder struct { | ||
| name string | ||
| owningJiraComponent string | ||
| responsiblePerson string | ||
| owningProduct OwningProduct | ||
| enhancementPRURL string | ||
|
|
||
| status []featureGateStatus | ||
|
|
||
| statusByClusterProfileByFeatureSet map[ClusterProfileName]map[configv1.FeatureSet]bool | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we still need this field or does it become effectively unused?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh yep, that's unused, will cleanup
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this still needs cleanup? |
||
| } | ||
| type featureGateStatus struct { | ||
| version sets.Set[uint64] | ||
| clusterProfile sets.Set[ClusterProfileName] | ||
| featureSets sets.Set[configv1.FeatureSet] | ||
| } | ||
everettraven marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| func (s *featureGateStatus) isEnabled(version uint64, clusterProfile ClusterProfileName, featureSet configv1.FeatureSet) bool { | ||
| // If either version or clusterprofile are empty, match all. | ||
| matchesVersion := len(s.version) == 0 || s.version.Has(version) | ||
| matchesClusterProfile := len(s.clusterProfile) == 0 || s.clusterProfile.Has(clusterProfile) | ||
|
|
||
| matchesFeatureSet := s.featureSets.Has(featureSet) | ||
|
|
||
| return matchesVersion && matchesClusterProfile && matchesFeatureSet | ||
| } | ||
|
|
||
| const ( | ||
| legacyFeatureGateWithoutEnhancement = "FeatureGate predates 4.18" | ||
|
|
@@ -95,19 +184,19 @@ func (b *featureGateBuilder) enhancementPR(url string) *featureGateBuilder { | |
| return b | ||
| } | ||
|
|
||
| func (b *featureGateBuilder) enableIn(featureSets ...configv1.FeatureSet) *featureGateBuilder { | ||
| for clusterProfile := range b.statusByClusterProfileByFeatureSet { | ||
| for _, featureSet := range featureSets { | ||
| b.statusByClusterProfileByFeatureSet[clusterProfile][featureSet] = true | ||
| } | ||
| func (b *featureGateBuilder) enable(opts ...featureGateEnableOption) *featureGateBuilder { | ||
| status := featureGateStatus{ | ||
| version: sets.New[uint64](), | ||
| clusterProfile: sets.New[ClusterProfileName](), | ||
| featureSets: sets.New[configv1.FeatureSet](), | ||
| } | ||
| return b | ||
| } | ||
|
|
||
| func (b *featureGateBuilder) enableForClusterProfile(clusterProfile ClusterProfileName, featureSets ...configv1.FeatureSet) *featureGateBuilder { | ||
| for _, featureSet := range featureSets { | ||
| b.statusByClusterProfileByFeatureSet[clusterProfile][featureSet] = true | ||
| for _, opt := range opts { | ||
| opt(&status) | ||
| } | ||
|
|
||
| b.status = append(b.status, status) | ||
|
|
||
| return b | ||
| } | ||
|
|
||
|
|
@@ -144,33 +233,8 @@ func (b *featureGateBuilder) register() (configv1.FeatureGateName, error) { | |
| } | ||
|
|
||
| featureGateName := configv1.FeatureGateName(b.name) | ||
| description := FeatureGateDescription{ | ||
| FeatureGateAttributes: configv1.FeatureGateAttributes{ | ||
| Name: featureGateName, | ||
| }, | ||
| OwningJiraComponent: b.owningJiraComponent, | ||
| ResponsiblePerson: b.responsiblePerson, | ||
| OwningProduct: b.owningProduct, | ||
| EnhancementPR: b.enhancementPRURL, | ||
| } | ||
|
|
||
| // statusByClusterProfileByFeatureSet is initialized by constructor to be false for every combination | ||
| for clusterProfile, byFeatureSet := range b.statusByClusterProfileByFeatureSet { | ||
| for featureSet, enabled := range byFeatureSet { | ||
| if _, ok := allFeatureGates[clusterProfile]; !ok { | ||
| allFeatureGates[clusterProfile] = map[configv1.FeatureSet]*FeatureGateEnabledDisabled{} | ||
| } | ||
| if _, ok := allFeatureGates[clusterProfile][featureSet]; !ok { | ||
| allFeatureGates[clusterProfile][featureSet] = &FeatureGateEnabledDisabled{} | ||
| } | ||
|
|
||
| if enabled { | ||
| allFeatureGates[clusterProfile][featureSet].Enabled = append(allFeatureGates[clusterProfile][featureSet].Enabled, description) | ||
| } else { | ||
| allFeatureGates[clusterProfile][featureSet].Disabled = append(allFeatureGates[clusterProfile][featureSet].Disabled, description) | ||
| } | ||
| } | ||
| } | ||
| allFeatureGates[featureGateName] = b.status | ||
|
|
||
| return featureGateName, nil | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regex pattern appears malformed.
The path pattern
features|payload-command/*.gouses glob-style*syntax, but golangci-lint uses Go regexp where*means "zero or more of the preceding character" (not "any characters"). Additionally, the.beforegomatches any single character rather than a literal dot.If the intent is to exclude all
.gofiles underfeatures/andpayload-command/directories, consider:🔧 Proposed fix
- linters: - kubeapilinter - path: features|payload-command/*.go + path: (features|payload-command)/.*\.goAlternatively, if you want to exclude entire directories regardless of file extension:
📝 Committable suggestion
🤖 Prompt for AI Agents