Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ linters:
# This regex must always be updated in tandem with the regex in .golangci.go-validated.yaml that prevents `optionalfields` from being applied to the files in the path.
path: machine/v1beta1/(types_awsprovider.go|types_azureprovider.go|types_gcpprovider.go|types_vsphereprovider.go)|machine/v1alpha1/types_openstack.go
text: "optionalfields"
- linters:
- kubeapilinter
path: features|payload-command/*.go
Comment on lines +109 to +111
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Regex pattern appears malformed.

The path pattern features|payload-command/*.go uses glob-style * syntax, but golangci-lint uses Go regexp where * means "zero or more of the preceding character" (not "any characters"). Additionally, the . before go matches any single character rather than a literal dot.

If the intent is to exclude all .go files under features/ and payload-command/ directories, consider:

🔧 Proposed fix
       - linters:
         - kubeapilinter
-        path: features|payload-command/*.go
+        path: (features|payload-command)/.*\.go

Alternatively, if you want to exclude entire directories regardless of file extension:

        path: (features|payload-command)/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- linters:
- kubeapilinter
path: features|payload-command/*.go
- linters:
- kubeapilinter
path: (features|payload-command)/.*\.go
🤖 Prompt for AI Agents
In @.golangci.yaml around lines 109 - 111, The path regexp
"features|payload-command/*.go" in .golangci.yaml is malformed for golangci-lint
(Go regexp), so replace it with a proper Go regexp that groups the directories,
uses ".*" for any characters and escapes the dot; e.g., set the path to
^(features|payload-command)/.*\.go$ to match .go files under those dirs, or use
^(features|payload-command)/ to exclude the entire directories regardless of
extension.

issues:
# We have a lot of existing issues.
# Want to make sure that those adding new fields have an
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ metadata:
api.openshift.io/merged-by-featuregates: "true"
include.release.openshift.io/self-managed-high-availability: "true"
release.openshift.io/feature-set: Default
release.openshift.io/major-version: 5,6,7,8,9,10
name: clusterversions.config.openshift.io
spec:
group: config.openshift.io
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ metadata:
api.openshift.io/merged-by-featuregates: "true"
include.release.openshift.io/self-managed-high-availability: "true"
release.openshift.io/feature-set: OKD
release.openshift.io/major-version: 5,6,7,8,9,10
name: clusterversions.config.openshift.io
spec:
group: config.openshift.io
Expand Down
288 changes: 163 additions & 125 deletions features/features.go

Large diffs are not rendered by default.

66 changes: 34 additions & 32 deletions features/okd_featureset_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,40 @@ import (
func TestOKDHasAllDefaultFeatureGates(t *testing.T) {
allFeatureSets := AllFeatureSets()

// Check each cluster profile
for clusterProfile, byFeatureSet := range allFeatureSets {
defaultGates, hasDefault := byFeatureSet[configv1.Default]
okdGates, hasOKD := byFeatureSet[configv1.OKD]

if !hasOKD || !hasDefault {
continue
}

// Collect enabled feature gate names from Default and OKD
defaultEnabled := sets.NewString()
for _, gate := range defaultGates.Enabled {
defaultEnabled.Insert(string(gate.FeatureGateAttributes.Name))
}

okdEnabled := sets.NewString()
for _, gate := range okdGates.Enabled {
okdEnabled.Insert(string(gate.FeatureGateAttributes.Name))
}

// Check that all Default featuregates are in OKD
missingInOKD := defaultEnabled.Difference(okdEnabled)

if missingInOKD.Len() > 0 {
missingList := missingInOKD.List()
sort.Strings(missingList)

t.Errorf("ClusterProfile %q: OKD featureset is missing %d featuregate(s) that are enabled in Default:\n - %s\n\nAll featuregates enabled in Default must also be enabled in OKD.",
clusterProfile,
missingInOKD.Len(),
strings.Join(missingList, "\n - "),
)
for _, byClusterProfileByFeatureSet := range allFeatureSets {
// Check each cluster profile
for clusterProfile, byFeatureSet := range byClusterProfileByFeatureSet {
defaultGates, hasDefault := byFeatureSet[configv1.Default]
okdGates, hasOKD := byFeatureSet[configv1.OKD]

if !hasOKD || !hasDefault {
continue
}

// Collect enabled feature gate names from Default and OKD
defaultEnabled := sets.NewString()
for _, gate := range defaultGates.Enabled {
defaultEnabled.Insert(string(gate.FeatureGateAttributes.Name))
}

okdEnabled := sets.NewString()
for _, gate := range okdGates.Enabled {
okdEnabled.Insert(string(gate.FeatureGateAttributes.Name))
}

// Check that all Default featuregates are in OKD
missingInOKD := defaultEnabled.Difference(okdEnabled)

if missingInOKD.Len() > 0 {
missingList := missingInOKD.List()
sort.Strings(missingList)

t.Errorf("ClusterProfile %q: OKD featureset is missing %d featuregate(s) that are enabled in Default:\n - %s\n\nAll featuregates enabled in Default must also be enabled in OKD.",
clusterProfile,
missingInOKD.Len(),
strings.Join(missingList, "\n - "),
)
}
}
}
}
136 changes: 100 additions & 36 deletions features/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 inDefault() automatically apply both?

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we still need this field or does it become effectively unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yep, that's unused, will cleanup

Copy link
Contributor

Choose a reason for hiding this comment

The 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]
}

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"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/openshift/api
go 1.24.0

require (
github.com/blang/semver/v4 v4.0.0
github.com/gogo/protobuf v1.3.2
golang.org/x/tools v0.26.0
k8s.io/api v0.34.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
1 change: 1 addition & 0 deletions hack/update-payload-featuregates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

source "$(dirname "${BASH_SOURCE}")/lib/init.sh"

rm -f ./payload-manifests/featuregates/*
go run --mod=vendor -trimpath github.com/openshift/api/payload-command/cmd/write-available-featuresets --asset-output-dir=./payload-manifests/featuregates

# Build codegen-crds when it's not present and not overridden for a specific file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ metadata:
annotations:
api-approved.openshift.io: https://github.com/openshift/api/pull/2255
api.openshift.io/merged-by-featuregates: "true"
include.release.openshift.io/ibm-cloud-managed: "true"
include.release.openshift.io/self-managed-high-availability: "true"
release.openshift.io/feature-set: Default
labels:
Expand Down
Loading