Skip to content

Commit e6efecc

Browse files
committed
pkg:types: add featuregates pkg
Adds a featuregate package to allow feature gating based on openshift/api. This commit copies code from other repos (acknowledged in the headers) rather than importing because the sources are well-contained and we want to minimize imports to pkg/types as pkg/types is intended to be imported by other codebases and by doing this we minimize their dependencies. This code is mostly borrowed from: https://github.com/openshift/cluster-config-operator/blob/636a2dc303037e2561a243ae1ab5c5b953ddad04/pkg/cmd/render/render.go#L153
1 parent eceb585 commit e6efecc

File tree

3 files changed

+160
-45
lines changed

3 files changed

+160
-45
lines changed

pkg/asset/manifests/featuregate.go

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package manifests
22

33
import (
44
"path/filepath"
5-
"strconv"
6-
"strings"
75

86
"github.com/pkg/errors"
97
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -12,6 +10,7 @@ import (
1210
configv1 "github.com/openshift/api/config/v1"
1311
"github.com/openshift/installer/pkg/asset"
1412
"github.com/openshift/installer/pkg/asset/installconfig"
13+
"github.com/openshift/installer/pkg/types/featuregates"
1514
)
1615

1716
var fgFileName = filepath.Join(openshiftManifestDir, "99_feature-gate.yaml")
@@ -62,7 +61,7 @@ func (f *FeatureGate) Generate(dependencies asset.Parents) error {
6261
return errors.Errorf("custom features can only be used with the CustomNoUpgrade feature set")
6362
}
6463

65-
customFeatures, err := generateCustomFeatures(installConfig.Config.FeatureGates)
64+
customFeatures, err := featuregates.GenerateCustomFeatures(installConfig.Config.FeatureGates)
6665
if err != nil {
6766
return errors.Wrapf(err, "failed to generate custom features")
6867
}
@@ -93,45 +92,3 @@ func (f *FeatureGate) Files() []*asset.File {
9392
func (f *FeatureGate) Load(ff asset.FileFetcher) (bool, error) {
9493
return false, nil
9594
}
96-
97-
// generateCustomFeatures generates the custom feature gates from the install config.
98-
func generateCustomFeatures(features []string) (*configv1.CustomFeatureGates, error) {
99-
customFeatures := &configv1.CustomFeatureGates{}
100-
101-
for _, feature := range features {
102-
featureName, enabled, err := parseCustomFeatureGate(feature)
103-
if err != nil {
104-
return nil, errors.Wrapf(err, "failed to parse custom feature %s", feature)
105-
}
106-
107-
if enabled {
108-
customFeatures.Enabled = append(customFeatures.Enabled, featureName)
109-
} else {
110-
customFeatures.Disabled = append(customFeatures.Disabled, featureName)
111-
}
112-
}
113-
114-
return customFeatures, nil
115-
}
116-
117-
// parseCustomFeatureGates parses the custom feature gate string into the feature name and whether it is enabled.
118-
// The expected format is <FeatureName>=<Enabled>.
119-
func parseCustomFeatureGate(rawFeature string) (configv1.FeatureGateName, bool, error) {
120-
var featureName string
121-
var enabled bool
122-
123-
featureParts := strings.Split(rawFeature, "=")
124-
if len(featureParts) != 2 {
125-
return "", false, errors.Errorf("feature not in expected format %s", rawFeature)
126-
}
127-
128-
featureName = featureParts[0]
129-
130-
var err error
131-
enabled, err = strconv.ParseBool(featureParts[1])
132-
if err != nil {
133-
return "", false, errors.Wrapf(err, "feature not in expected format %s, could not parse boolean value", rawFeature)
134-
}
135-
136-
return configv1.FeatureGateName(featureName), enabled, nil
137-
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package featuregates
2+
3+
// source: https://github.com/openshift/library-go/blob/c515269de16e5e239bd6e93e1f9821a976bb460b/pkg/operator/configobserver/featuregates/featuregate.go#L23-L28
4+
5+
import (
6+
"fmt"
7+
8+
"k8s.io/apimachinery/pkg/util/sets"
9+
"k8s.io/apimachinery/pkg/util/validation/field"
10+
11+
configv1 "github.com/openshift/api/config/v1"
12+
)
13+
14+
// FeatureGate indicates whether a given feature is enabled or not
15+
// This interface is heavily influenced by k8s.io/component-base, but not exactly compatible.
16+
type FeatureGate interface {
17+
// Enabled returns true if the key is enabled.
18+
Enabled(key configv1.FeatureGateName) bool
19+
}
20+
21+
type featureGate struct {
22+
enabled sets.Set[configv1.FeatureGateName]
23+
disabled sets.Set[configv1.FeatureGateName]
24+
}
25+
26+
// GatedInstallConfigFeature contains fields that will be used to validate
27+
// that required feature gates are enabled when gated install config fields
28+
// are used.
29+
// FeatureGateName: openshift/api feature gate required to enable the use of Field
30+
// Condition: the check which determines whether the install config field is used,
31+
// if Condition evaluates to True, FeatureGateName must be enabled to pass validation.
32+
// Field: the gated install config field, Field is used in the error message.
33+
type GatedInstallConfigFeature struct {
34+
FeatureGateName configv1.FeatureGateName
35+
Condition bool
36+
Field *field.Path
37+
}
38+
39+
func newFeatureGate(enabled, disabled []configv1.FeatureGateName) FeatureGate {
40+
return &featureGate{
41+
enabled: sets.New[configv1.FeatureGateName](enabled...),
42+
disabled: sets.New[configv1.FeatureGateName](disabled...),
43+
}
44+
}
45+
46+
// Enabled returns true if the provided feature gate is enabled
47+
// in the active feature sets.
48+
func (f *featureGate) Enabled(key configv1.FeatureGateName) bool {
49+
if f.enabled.Has(key) {
50+
return true
51+
}
52+
if f.disabled.Has(key) {
53+
return false
54+
}
55+
56+
panic(fmt.Errorf("feature %q is not registered in FeatureGates", key))
57+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package featuregates
2+
3+
// source: https://github.com/openshift/cluster-config-operator/blob/636a2dc303037e2561a243ae1ab5c5b953ddad04/pkg/cmd/render/render.go#L153
4+
5+
import (
6+
"fmt"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/pkg/errors"
11+
"k8s.io/apimachinery/pkg/util/sets"
12+
13+
configv1 "github.com/openshift/api/config/v1"
14+
)
15+
16+
func toFeatureGateNames(in []configv1.FeatureGateDescription) []configv1.FeatureGateName {
17+
out := []configv1.FeatureGateName{}
18+
for _, curr := range in {
19+
out = append(out, curr.FeatureGateAttributes.Name)
20+
}
21+
22+
return out
23+
}
24+
25+
// completeFeatureGates identifies every known feature and ensures that is explicitly on or explicitly off.
26+
func completeFeatureGates(knownFeatureSets map[configv1.FeatureSet]*configv1.FeatureGateEnabledDisabled, enabled, disabled []configv1.FeatureGateName) ([]configv1.FeatureGateName, []configv1.FeatureGateName) {
27+
specificallyEnabledFeatureGates := sets.New[configv1.FeatureGateName]()
28+
specificallyEnabledFeatureGates.Insert(enabled...)
29+
30+
knownFeatureGates := sets.New[configv1.FeatureGateName]()
31+
knownFeatureGates.Insert(enabled...)
32+
knownFeatureGates.Insert(disabled...)
33+
for _, known := range knownFeatureSets {
34+
for _, curr := range known.Disabled {
35+
knownFeatureGates.Insert(curr.FeatureGateAttributes.Name)
36+
}
37+
for _, curr := range known.Enabled {
38+
knownFeatureGates.Insert(curr.FeatureGateAttributes.Name)
39+
}
40+
}
41+
42+
return enabled, knownFeatureGates.Difference(specificallyEnabledFeatureGates).UnsortedList()
43+
}
44+
45+
// FeatureGateFromFeatureSets creates a FeatureGate from the active feature sets.
46+
func FeatureGateFromFeatureSets(knownFeatureSets map[configv1.FeatureSet]*configv1.FeatureGateEnabledDisabled, fs configv1.FeatureSet, customFS *configv1.CustomFeatureGates) (FeatureGate, error) {
47+
if customFS != nil {
48+
completeEnabled, completeDisabled := completeFeatureGates(knownFeatureSets, customFS.Enabled, customFS.Disabled)
49+
return newFeatureGate(completeEnabled, completeDisabled), nil
50+
}
51+
52+
featureSet, ok := knownFeatureSets[fs]
53+
if !ok {
54+
return nil, fmt.Errorf(".spec.featureSet %q not found", featureSet)
55+
}
56+
57+
completeEnabled, completeDisabled := completeFeatureGates(knownFeatureSets, toFeatureGateNames(featureSet.Enabled), toFeatureGateNames(featureSet.Disabled))
58+
return newFeatureGate(completeEnabled, completeDisabled), nil
59+
}
60+
61+
// GenerateCustomFeatures generates the custom feature gates from the install config.
62+
func GenerateCustomFeatures(features []string) (*configv1.CustomFeatureGates, error) {
63+
customFeatures := &configv1.CustomFeatureGates{}
64+
65+
for _, feature := range features {
66+
featureName, enabled, err := parseCustomFeatureGate(feature)
67+
if err != nil {
68+
return nil, errors.Wrapf(err, "failed to parse custom feature %s", feature)
69+
}
70+
71+
if enabled {
72+
customFeatures.Enabled = append(customFeatures.Enabled, featureName)
73+
} else {
74+
customFeatures.Disabled = append(customFeatures.Disabled, featureName)
75+
}
76+
}
77+
78+
return customFeatures, nil
79+
}
80+
81+
// parseCustomFeatureGates parses the custom feature gate string into the feature name and whether it is enabled.
82+
// The expected format is <FeatureName>=<Enabled>.
83+
func parseCustomFeatureGate(rawFeature string) (configv1.FeatureGateName, bool, error) {
84+
var featureName string
85+
var enabled bool
86+
87+
featureParts := strings.Split(rawFeature, "=")
88+
if len(featureParts) != 2 {
89+
return "", false, errors.Errorf("feature not in expected format %s", rawFeature)
90+
}
91+
92+
featureName = featureParts[0]
93+
94+
var err error
95+
enabled, err = strconv.ParseBool(featureParts[1])
96+
if err != nil {
97+
return "", false, errors.Wrapf(err, "feature not in expected format %s, could not parse boolean value", rawFeature)
98+
}
99+
100+
return configv1.FeatureGateName(featureName), enabled, nil
101+
}

0 commit comments

Comments
 (0)