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
280 changes: 155 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 - "),
)
}
}
}
}
107 changes: 70 additions & 37 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,72 @@ var (
kubernetes = OwningProduct("Kubernetes")
)

type featureGateEnableOption func(s *featureGateStatus)

func inVersion(version uint64) featureGateEnableOption {
return func(s *featureGateStatus) {
s.version.Insert(version)
}
}

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)
}

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
}
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 +153,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 +202,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
23 changes: 12 additions & 11 deletions payload-command/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package render
import (
"flag"
"fmt"
"github.com/openshift/api/features"
"os"
"path/filepath"
"sort"
"strings"

"github.com/blang/semver/v4"
"github.com/openshift/api/features"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"

Expand Down Expand Up @@ -68,6 +70,11 @@ func (o *RenderOpts) Run() error {
}
clusterProfileAnnotationName := fmt.Sprintf("include.release.openshift.io/%s", o.UnprefixedClusterProfile)

version, err := semver.ParseTolerant(o.PayloadVersion)
if err != nil {
return fmt.Errorf("problem parsing payload version: %w", err)
}

for _, featureGateFile := range featureGateFiles {
uncastObj, err := featureGateFile.GetDecodedObj()
if err != nil {
Expand All @@ -84,7 +91,7 @@ func (o *RenderOpts) Run() error {

if featureGates.Spec.FeatureSet == configv1.CustomNoUpgrade {
featureSet = string(featureGates.Spec.FeatureSet)
renderedFeatureGates, err := renderCustomNoUpgradeFeatureGate(featureGates, features.ClusterProfileName(clusterProfileAnnotationName), o.PayloadVersion)
renderedFeatureGates, err := renderCustomNoUpgradeFeatureGate(featureGates, features.ClusterProfileName(clusterProfileAnnotationName), o.PayloadVersion, version.Major)
if err != nil {
return err
}
Expand All @@ -105,10 +112,7 @@ func (o *RenderOpts) Run() error {
featureGates.Annotations[clusterProfileAnnotationName] = "true"
}

featureGateStatus, err := features.FeatureSets(features.ClusterProfileName(clusterProfileAnnotationName), featureGates.Spec.FeatureSet)
if err != nil {
return fmt.Errorf("unable to resolve featureGateStatus: %w", err)
}
featureGateStatus := features.FeatureSets(version.Major, features.ClusterProfileName(clusterProfileAnnotationName), featureGates.Spec.FeatureSet)
currentDetails := FeaturesGateDetailsFromFeatureSets(featureGateStatus, o.PayloadVersion)
featureGates.Status.FeatureGates = []configv1.FeatureGateDetails{*currentDetails}

Expand All @@ -133,7 +137,7 @@ func (o *RenderOpts) Run() error {
return nil
}

func renderCustomNoUpgradeFeatureGate(in *configv1.FeatureGate, clusterProfile features.ClusterProfileName, payloadVersion string) (*configv1.FeatureGate, error) {
func renderCustomNoUpgradeFeatureGate(in *configv1.FeatureGate, clusterProfile features.ClusterProfileName, payloadVersion string, payloadMajorVersion uint64) (*configv1.FeatureGate, error) {
if in.Spec.FeatureSet != configv1.CustomNoUpgrade {
return nil, fmt.Errorf("not CustomNoUpgrade")
}
Expand Down Expand Up @@ -162,10 +166,7 @@ func renderCustomNoUpgradeFeatureGate(in *configv1.FeatureGate, clusterProfile f
})
}

defaultFeatureGates, err := features.FeatureSets(clusterProfile, configv1.Default)
if err != nil {
return nil, err
}
defaultFeatureGates := features.FeatureSets(payloadMajorVersion, clusterProfile, configv1.Default)

enabled := []configv1.FeatureGateAttributes{}
disabled := []configv1.FeatureGateAttributes{}
Expand Down
Loading