From bd7691c88dda6b935148dd90ea622d87ab666b96 Mon Sep 17 00:00:00 2001 From: "Kistner, Dominic" Date: Wed, 22 Jun 2022 13:08:41 +0200 Subject: [PATCH] Put managed application credential behind a feature gate --- .../app/app.go | 5 + .../main.go | 2 + hack/api-reference/config.md | 26 +- pkg/apis/config/types.go | 6 +- pkg/apis/config/v1alpha1/types.go | 8 +- .../v1alpha1/zz_generated.conversion.go | 4 +- .../config/v1alpha1/zz_generated.deepcopy.go | 7 + pkg/apis/config/validation/config.go | 4 - pkg/apis/config/validation/config_test.go | 10 - pkg/apis/config/zz_generated.deepcopy.go | 7 + pkg/features/features.go | 37 ++ .../managedappcredential/manager_ensure.go | 3 +- .../k8s.io/component-base/featuregate/OWNERS | 17 + .../featuregate/feature_gate.go | 375 ++++++++++++++++++ vendor/modules.txt | 1 + 15 files changed, 478 insertions(+), 34 deletions(-) create mode 100644 pkg/features/features.go create mode 100644 vendor/k8s.io/component-base/featuregate/OWNERS create mode 100644 vendor/k8s.io/component-base/featuregate/feature_gate.go diff --git a/cmd/gardener-extension-provider-openstack/app/app.go b/cmd/gardener-extension-provider-openstack/app/app.go index 9b5d271c4..3d7b7aee3 100644 --- a/cmd/gardener-extension-provider-openstack/app/app.go +++ b/cmd/gardener-extension-provider-openstack/app/app.go @@ -31,6 +31,7 @@ import ( "github.com/gardener/gardener-extension-provider-openstack/pkg/controller/healthcheck" openstackinfrastructure "github.com/gardener/gardener-extension-provider-openstack/pkg/controller/infrastructure" openstackworker "github.com/gardener/gardener-extension-provider-openstack/pkg/controller/worker" + "github.com/gardener/gardener-extension-provider-openstack/pkg/features" "github.com/gardener/gardener-extension-provider-openstack/pkg/openstack" openstackcontrolplaneexposure "github.com/gardener/gardener-extension-provider-openstack/pkg/webhook/controlplaneexposure" @@ -168,6 +169,10 @@ func NewControllerManagerCommand(ctx context.Context) *cobra.Command { return fmt.Errorf("invalid controller config: %w", err) } + if err := features.ExtensionFeatureGate.SetFromMap(configFileOpts.Completed().Config.FeatureGates); err != nil { + return err + } + util.ApplyClientConnectionConfigurationToRESTConfig(configFileOpts.Completed().Config.ClientConnection, restOpts.Completed().Config) if workerReconcileOpts.Completed().DeployCRDs { diff --git a/cmd/gardener-extension-provider-openstack/main.go b/cmd/gardener-extension-provider-openstack/main.go index a663152c9..807bd7ff8 100644 --- a/cmd/gardener-extension-provider-openstack/main.go +++ b/cmd/gardener-extension-provider-openstack/main.go @@ -18,6 +18,7 @@ import ( "os" "github.com/gardener/gardener-extension-provider-openstack/cmd/gardener-extension-provider-openstack/app" + "github.com/gardener/gardener-extension-provider-openstack/pkg/features" "github.com/gardener/gardener/pkg/logger" runtimelog "sigs.k8s.io/controller-runtime/pkg/log" @@ -25,6 +26,7 @@ import ( ) func main() { + features.RegisterExtensionFeatureGate() runtimelog.SetLogger(logger.ZapLogger(false)) cmd := app.NewControllerManagerCommand(signals.SetupSignalHandler()) diff --git a/hack/api-reference/config.md b/hack/api-reference/config.md index 5341ae480..e93c9d6b4 100644 --- a/hack/api-reference/config.md +++ b/hack/api-reference/config.md @@ -112,6 +112,20 @@ ApplicationCredentialConfig

ApplicationCrednentialConfig defines the configuration for managed application credentials.

+ + +featureGates
+ +map[string]bool + + + +(Optional) +

FeatureGates is a map of feature names to bools that enable +or disable alpha/experimental features. +Default: nil

+ +

ApplicationCredentialConfig @@ -133,18 +147,6 @@ ApplicationCredentialConfig -enabled
- -bool - - - -(Optional) -

Enabled indicate if managed application credentials should be used.

- - - - lifetime
diff --git a/pkg/apis/config/types.go b/pkg/apis/config/types.go index b20204641..8ecbbf278 100644 --- a/pkg/apis/config/types.go +++ b/pkg/apis/config/types.go @@ -39,6 +39,10 @@ type ControllerConfiguration struct { BastionConfig *BastionConfig // ApplicationCrednentialConfig defines the configuration for managed application credentials. ApplicationCredentialConfig *ApplicationCredentialConfig + // FeatureGates is a map of feature names to bools that enable + // or disable alpha/experimental features. + // Default: nil + FeatureGates map[string]bool } // ETCD is an etcd configuration. @@ -73,8 +77,6 @@ type BastionConfig struct { // ApplicationCredentialConfig defines the configuration for managed application credentials. type ApplicationCredentialConfig struct { - // Enabled indicate if managed application credentials should be used. - Enabled bool // Lifetime define how long a managed application credentials are valid. // Once the creation time + lifetime of an application credential is expired // it will be renewed once it is next reconciled. diff --git a/pkg/apis/config/v1alpha1/types.go b/pkg/apis/config/v1alpha1/types.go index aec48e4f8..0ea5824f3 100644 --- a/pkg/apis/config/v1alpha1/types.go +++ b/pkg/apis/config/v1alpha1/types.go @@ -44,6 +44,11 @@ type ControllerConfiguration struct { // ApplicationCrednentialConfig defines the configuration for managed application credentials. // +optional ApplicationCredentialConfig *ApplicationCredentialConfig `json:"managedApplicationCredential,omitempty"` + // FeatureGates is a map of feature names to bools that enable + // or disable alpha/experimental features. + // Default: nil + // +optional + FeatureGates map[string]bool `json:"featureGates,omitempty"` } // ETCD is an etcd configuration. @@ -81,9 +86,6 @@ type BastionConfig struct { // ApplicationCredentialConfig defines the configuration for managed application credentials. type ApplicationCredentialConfig struct { - // Enabled indicate if managed application credentials should be used. - // +optional - Enabled bool `json:"enabled,omitempty"` // Lifetime define how long a managed application credentials are valid. // Once the creation time + lifetime of an application credential is expired // it will be renewed once it is next reconciled. diff --git a/pkg/apis/config/v1alpha1/zz_generated.conversion.go b/pkg/apis/config/v1alpha1/zz_generated.conversion.go index da7af497a..a5e9bad08 100644 --- a/pkg/apis/config/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/config/v1alpha1/zz_generated.conversion.go @@ -106,7 +106,6 @@ func RegisterConversions(s *runtime.Scheme) error { } func autoConvert_v1alpha1_ApplicationCredentialConfig_To_config_ApplicationCredentialConfig(in *ApplicationCredentialConfig, out *config.ApplicationCredentialConfig, s conversion.Scope) error { - out.Enabled = in.Enabled out.Lifetime = (*v1.Duration)(unsafe.Pointer(in.Lifetime)) out.OpenstackExpirationPeriod = (*v1.Duration)(unsafe.Pointer(in.OpenstackExpirationPeriod)) out.RenewThreshold = (*v1.Duration)(unsafe.Pointer(in.RenewThreshold)) @@ -119,7 +118,6 @@ func Convert_v1alpha1_ApplicationCredentialConfig_To_config_ApplicationCredentia } func autoConvert_config_ApplicationCredentialConfig_To_v1alpha1_ApplicationCredentialConfig(in *config.ApplicationCredentialConfig, out *ApplicationCredentialConfig, s conversion.Scope) error { - out.Enabled = in.Enabled out.Lifetime = (*v1.Duration)(unsafe.Pointer(in.Lifetime)) out.OpenstackExpirationPeriod = (*v1.Duration)(unsafe.Pointer(in.OpenstackExpirationPeriod)) out.RenewThreshold = (*v1.Duration)(unsafe.Pointer(in.RenewThreshold)) @@ -161,6 +159,7 @@ func autoConvert_v1alpha1_ControllerConfiguration_To_config_ControllerConfigurat out.HealthCheckConfig = (*healthcheckconfig.HealthCheckConfig)(unsafe.Pointer(in.HealthCheckConfig)) out.BastionConfig = (*config.BastionConfig)(unsafe.Pointer(in.BastionConfig)) out.ApplicationCredentialConfig = (*config.ApplicationCredentialConfig)(unsafe.Pointer(in.ApplicationCredentialConfig)) + out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) return nil } @@ -177,6 +176,7 @@ func autoConvert_config_ControllerConfiguration_To_v1alpha1_ControllerConfigurat out.HealthCheckConfig = (*healthcheckconfigv1alpha1.HealthCheckConfig)(unsafe.Pointer(in.HealthCheckConfig)) out.BastionConfig = (*BastionConfig)(unsafe.Pointer(in.BastionConfig)) out.ApplicationCredentialConfig = (*ApplicationCredentialConfig)(unsafe.Pointer(in.ApplicationCredentialConfig)) + out.FeatureGates = *(*map[string]bool)(unsafe.Pointer(&in.FeatureGates)) return nil } diff --git a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go index bae3f72fa..18fdde2e0 100644 --- a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go @@ -100,6 +100,13 @@ func (in *ControllerConfiguration) DeepCopyInto(out *ControllerConfiguration) { *out = new(ApplicationCredentialConfig) (*in).DeepCopyInto(*out) } + if in.FeatureGates != nil { + in, out := &in.FeatureGates, &out.FeatureGates + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/pkg/apis/config/validation/config.go b/pkg/apis/config/validation/config.go index 483058603..69cf76aff 100644 --- a/pkg/apis/config/validation/config.go +++ b/pkg/apis/config/validation/config.go @@ -37,10 +37,6 @@ func ValidateControllerConfig(cfg *config.ControllerConfiguration) error { func validateApplicationCredentialConfig(cfg *config.ApplicationCredentialConfig, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - if !cfg.Enabled { - return allErrs - } - if cfg.Lifetime.Duration > cfg.OpenstackExpirationPeriod.Duration { allErrs = append(allErrs, field.Invalid(fldPath.Child("lifetime"), cfg.Lifetime.Duration, "application credential lifetime cannot be lower than the openstack layer expiration time")) } diff --git a/pkg/apis/config/validation/config_test.go b/pkg/apis/config/validation/config_test.go index 7932f99ea..efbe69094 100644 --- a/pkg/apis/config/validation/config_test.go +++ b/pkg/apis/config/validation/config_test.go @@ -35,25 +35,15 @@ var _ = Describe("ControllerConfig validation", func() { Entry("should pass with valid application credential config", &config.ControllerConfiguration{ ApplicationCredentialConfig: &config.ApplicationCredentialConfig{ - Enabled: true, Lifetime: &metav1.Duration{Duration: 24 * time.Hour}, OpenstackExpirationPeriod: &metav1.Duration{Duration: 72 * time.Hour}, }, }, BeNil(), ), - Entry("should pass when application credential usage is disabled", - &config.ControllerConfiguration{ - ApplicationCredentialConfig: &config.ApplicationCredentialConfig{ - Enabled: false, - }, - }, - BeNil(), - ), Entry("should forbid application credential lifetime larger than openstack expiration time", &config.ControllerConfiguration{ ApplicationCredentialConfig: &config.ApplicationCredentialConfig{ - Enabled: true, Lifetime: &metav1.Duration{Duration: 73 * time.Hour}, OpenstackExpirationPeriod: &metav1.Duration{Duration: 72 * time.Hour}, }, diff --git a/pkg/apis/config/zz_generated.deepcopy.go b/pkg/apis/config/zz_generated.deepcopy.go index d89411a03..c1d0aa4f0 100644 --- a/pkg/apis/config/zz_generated.deepcopy.go +++ b/pkg/apis/config/zz_generated.deepcopy.go @@ -100,6 +100,13 @@ func (in *ControllerConfiguration) DeepCopyInto(out *ControllerConfiguration) { *out = new(ApplicationCredentialConfig) (*in).DeepCopyInto(*out) } + if in.FeatureGates != nil { + in, out := &in.FeatureGates, &out.FeatureGates + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/pkg/features/features.go b/pkg/features/features.go new file mode 100644 index 000000000..a12649cbb --- /dev/null +++ b/pkg/features/features.go @@ -0,0 +1,37 @@ +// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package features + +import ( + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/component-base/featuregate" +) + +const ( + // ManagedApplicationCredential allows to manage application credentials for Shoots + // to interact with the Openstack layer on behalf of an Openstack user. + // owner @dkistner + // alpha: v1.27.0 + ManagedApplicationCredential featuregate.Feature = "ManagedApplicationCredential" +) + +var ExtensionFeatureGate = featuregate.NewFeatureGate() + +// RegisterExtensionFeatureGate registers features to the extension feature gate. +func RegisterExtensionFeatureGate() { + runtime.Must(ExtensionFeatureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ + ManagedApplicationCredential: {Default: false, PreRelease: featuregate.Alpha}, + })) +} diff --git a/pkg/internal/managedappcredential/manager_ensure.go b/pkg/internal/managedappcredential/manager_ensure.go index d2bac5f22..7edc0392f 100644 --- a/pkg/internal/managedappcredential/manager_ensure.go +++ b/pkg/internal/managedappcredential/manager_ensure.go @@ -19,6 +19,7 @@ import ( "fmt" "time" + "github.com/gardener/gardener-extension-provider-openstack/pkg/features" "github.com/gardener/gardener-extension-provider-openstack/pkg/openstack" openstackclient "github.com/gardener/gardener-extension-provider-openstack/pkg/openstack/client" "github.com/gardener/gardener/pkg/utils" @@ -76,7 +77,7 @@ func (m *Manager) Ensure(ctx context.Context, credentials *openstack.Credentials // In case the application credential usage is disabled or the new parent user // itself is an appplication, it is tried to clean up old application credentials before aborting. - if !m.config.Enabled || newParentUser.isApplicationCredential() { + if newParentUser.isApplicationCredential() || !features.ExtensionFeatureGate.Enabled(features.ManagedApplicationCredential) { if oldParentUserUsable { if err := m.runGarbageCollection(ctx, oldParentUser, nil); err != nil { return err diff --git a/vendor/k8s.io/component-base/featuregate/OWNERS b/vendor/k8s.io/component-base/featuregate/OWNERS new file mode 100644 index 000000000..48e811bf4 --- /dev/null +++ b/vendor/k8s.io/component-base/featuregate/OWNERS @@ -0,0 +1,17 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +# Currently assigned to api-approvers since feature gates are the API +# for enabling/disabling other APIs. + +# Disable inheritance as this is an api owners file +options: + no_parent_owners: true +approvers: +- api-approvers +reviewers: +- api-reviewers + +labels: +- kind/api-change +- sig/api-machinery +- sig/cluster-lifecycle diff --git a/vendor/k8s.io/component-base/featuregate/feature_gate.go b/vendor/k8s.io/component-base/featuregate/feature_gate.go new file mode 100644 index 000000000..c7166d80b --- /dev/null +++ b/vendor/k8s.io/component-base/featuregate/feature_gate.go @@ -0,0 +1,375 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package featuregate + +import ( + "fmt" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + + "github.com/spf13/pflag" + + "k8s.io/apimachinery/pkg/util/naming" + "k8s.io/klog/v2" +) + +type Feature string + +const ( + flagName = "feature-gates" + + // allAlphaGate is a global toggle for alpha features. Per-feature key + // values override the default set by allAlphaGate. Examples: + // AllAlpha=false,NewFeature=true will result in newFeature=true + // AllAlpha=true,NewFeature=false will result in newFeature=false + allAlphaGate Feature = "AllAlpha" + + // allBetaGate is a global toggle for beta features. Per-feature key + // values override the default set by allBetaGate. Examples: + // AllBeta=false,NewFeature=true will result in NewFeature=true + // AllBeta=true,NewFeature=false will result in NewFeature=false + allBetaGate Feature = "AllBeta" +) + +var ( + // The generic features. + defaultFeatures = map[Feature]FeatureSpec{ + allAlphaGate: {Default: false, PreRelease: Alpha}, + allBetaGate: {Default: false, PreRelease: Beta}, + } + + // Special handling for a few gates. + specialFeatures = map[Feature]func(known map[Feature]FeatureSpec, enabled map[Feature]bool, val bool){ + allAlphaGate: setUnsetAlphaGates, + allBetaGate: setUnsetBetaGates, + } +) + +type FeatureSpec struct { + // Default is the default enablement state for the feature + Default bool + // LockToDefault indicates that the feature is locked to its default and cannot be changed + LockToDefault bool + // PreRelease indicates the maturity level of the feature + PreRelease prerelease +} + +type prerelease string + +const ( + // Values for PreRelease. + Alpha = prerelease("ALPHA") + Beta = prerelease("BETA") + GA = prerelease("") + + // Deprecated + Deprecated = prerelease("DEPRECATED") +) + +// FeatureGate indicates whether a given feature is enabled or not +type FeatureGate interface { + // Enabled returns true if the key is enabled. + Enabled(key Feature) bool + // KnownFeatures returns a slice of strings describing the FeatureGate's known features. + KnownFeatures() []string + // DeepCopy returns a deep copy of the FeatureGate object, such that gates can be + // set on the copy without mutating the original. This is useful for validating + // config against potential feature gate changes before committing those changes. + DeepCopy() MutableFeatureGate +} + +// MutableFeatureGate parses and stores flag gates for known features from +// a string like feature1=true,feature2=false,... +type MutableFeatureGate interface { + FeatureGate + + // AddFlag adds a flag for setting global feature gates to the specified FlagSet. + AddFlag(fs *pflag.FlagSet) + // Set parses and stores flag gates for known features + // from a string like feature1=true,feature2=false,... + Set(value string) error + // SetFromMap stores flag gates for known features from a map[string]bool or returns an error + SetFromMap(m map[string]bool) error + // Add adds features to the featureGate. + Add(features map[Feature]FeatureSpec) error + // GetAll returns a copy of the map of known feature names to feature specs. + GetAll() map[Feature]FeatureSpec +} + +// featureGate implements FeatureGate as well as pflag.Value for flag parsing. +type featureGate struct { + featureGateName string + + special map[Feature]func(map[Feature]FeatureSpec, map[Feature]bool, bool) + + // lock guards writes to known, enabled, and reads/writes of closed + lock sync.Mutex + // known holds a map[Feature]FeatureSpec + known *atomic.Value + // enabled holds a map[Feature]bool + enabled *atomic.Value + // closed is set to true when AddFlag is called, and prevents subsequent calls to Add + closed bool +} + +func setUnsetAlphaGates(known map[Feature]FeatureSpec, enabled map[Feature]bool, val bool) { + for k, v := range known { + if v.PreRelease == Alpha { + if _, found := enabled[k]; !found { + enabled[k] = val + } + } + } +} + +func setUnsetBetaGates(known map[Feature]FeatureSpec, enabled map[Feature]bool, val bool) { + for k, v := range known { + if v.PreRelease == Beta { + if _, found := enabled[k]; !found { + enabled[k] = val + } + } + } +} + +// Set, String, and Type implement pflag.Value +var _ pflag.Value = &featureGate{} + +// internalPackages are packages that ignored when creating a name for featureGates. These packages are in the common +// call chains, so they'd be unhelpful as names. +var internalPackages = []string{"k8s.io/component-base/featuregate/feature_gate.go"} + +func NewFeatureGate() *featureGate { + known := map[Feature]FeatureSpec{} + for k, v := range defaultFeatures { + known[k] = v + } + + knownValue := &atomic.Value{} + knownValue.Store(known) + + enabled := map[Feature]bool{} + enabledValue := &atomic.Value{} + enabledValue.Store(enabled) + + f := &featureGate{ + featureGateName: naming.GetNameFromCallsite(internalPackages...), + known: knownValue, + special: specialFeatures, + enabled: enabledValue, + } + return f +} + +// Set parses a string of the form "key1=value1,key2=value2,..." into a +// map[string]bool of known keys or returns an error. +func (f *featureGate) Set(value string) error { + m := make(map[string]bool) + for _, s := range strings.Split(value, ",") { + if len(s) == 0 { + continue + } + arr := strings.SplitN(s, "=", 2) + k := strings.TrimSpace(arr[0]) + if len(arr) != 2 { + return fmt.Errorf("missing bool value for %s", k) + } + v := strings.TrimSpace(arr[1]) + boolValue, err := strconv.ParseBool(v) + if err != nil { + return fmt.Errorf("invalid value of %s=%s, err: %v", k, v, err) + } + m[k] = boolValue + } + return f.SetFromMap(m) +} + +// SetFromMap stores flag gates for known features from a map[string]bool or returns an error +func (f *featureGate) SetFromMap(m map[string]bool) error { + f.lock.Lock() + defer f.lock.Unlock() + + // Copy existing state + known := map[Feature]FeatureSpec{} + for k, v := range f.known.Load().(map[Feature]FeatureSpec) { + known[k] = v + } + enabled := map[Feature]bool{} + for k, v := range f.enabled.Load().(map[Feature]bool) { + enabled[k] = v + } + + for k, v := range m { + k := Feature(k) + featureSpec, ok := known[k] + if !ok { + return fmt.Errorf("unrecognized feature gate: %s", k) + } + if featureSpec.LockToDefault && featureSpec.Default != v { + return fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, featureSpec.Default) + } + enabled[k] = v + // Handle "special" features like "all alpha gates" + if fn, found := f.special[k]; found { + fn(known, enabled, v) + } + + if featureSpec.PreRelease == Deprecated { + klog.Warningf("Setting deprecated feature gate %s=%t. It will be removed in a future release.", k, v) + } else if featureSpec.PreRelease == GA { + klog.Warningf("Setting GA feature gate %s=%t. It will be removed in a future release.", k, v) + } + } + + // Persist changes + f.known.Store(known) + f.enabled.Store(enabled) + + klog.V(1).Infof("feature gates: %v", f.enabled) + return nil +} + +// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,...". +func (f *featureGate) String() string { + pairs := []string{} + for k, v := range f.enabled.Load().(map[Feature]bool) { + pairs = append(pairs, fmt.Sprintf("%s=%t", k, v)) + } + sort.Strings(pairs) + return strings.Join(pairs, ",") +} + +func (f *featureGate) Type() string { + return "mapStringBool" +} + +// Add adds features to the featureGate. +func (f *featureGate) Add(features map[Feature]FeatureSpec) error { + f.lock.Lock() + defer f.lock.Unlock() + + if f.closed { + return fmt.Errorf("cannot add a feature gate after adding it to the flag set") + } + + // Copy existing state + known := map[Feature]FeatureSpec{} + for k, v := range f.known.Load().(map[Feature]FeatureSpec) { + known[k] = v + } + + for name, spec := range features { + if existingSpec, found := known[name]; found { + if existingSpec == spec { + continue + } + return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec) + } + + known[name] = spec + } + + // Persist updated state + f.known.Store(known) + + return nil +} + +// GetAll returns a copy of the map of known feature names to feature specs. +func (f *featureGate) GetAll() map[Feature]FeatureSpec { + retval := map[Feature]FeatureSpec{} + for k, v := range f.known.Load().(map[Feature]FeatureSpec) { + retval[k] = v + } + return retval +} + +// Enabled returns true if the key is enabled. If the key is not known, this call will panic. +func (f *featureGate) Enabled(key Feature) bool { + if v, ok := f.enabled.Load().(map[Feature]bool)[key]; ok { + return v + } + if v, ok := f.known.Load().(map[Feature]FeatureSpec)[key]; ok { + return v.Default + } + + panic(fmt.Errorf("feature %q is not registered in FeatureGate %q", key, f.featureGateName)) +} + +// AddFlag adds a flag for setting global feature gates to the specified FlagSet. +func (f *featureGate) AddFlag(fs *pflag.FlagSet) { + f.lock.Lock() + // TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead? + // Not all components expose a feature gates flag using this AddFlag method, and + // in the future, all components will completely stop exposing a feature gates flag, + // in favor of componentconfig. + f.closed = true + f.lock.Unlock() + + known := f.KnownFeatures() + fs.Var(f, flagName, ""+ + "A set of key=value pairs that describe feature gates for alpha/experimental features. "+ + "Options are:\n"+strings.Join(known, "\n")) +} + +// KnownFeatures returns a slice of strings describing the FeatureGate's known features. +// Deprecated and GA features are hidden from the list. +func (f *featureGate) KnownFeatures() []string { + var known []string + for k, v := range f.known.Load().(map[Feature]FeatureSpec) { + if v.PreRelease == GA || v.PreRelease == Deprecated { + continue + } + known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, v.PreRelease, v.Default)) + } + sort.Strings(known) + return known +} + +// DeepCopy returns a deep copy of the FeatureGate object, such that gates can be +// set on the copy without mutating the original. This is useful for validating +// config against potential feature gate changes before committing those changes. +func (f *featureGate) DeepCopy() MutableFeatureGate { + // Copy existing state. + known := map[Feature]FeatureSpec{} + for k, v := range f.known.Load().(map[Feature]FeatureSpec) { + known[k] = v + } + enabled := map[Feature]bool{} + for k, v := range f.enabled.Load().(map[Feature]bool) { + enabled[k] = v + } + + // Store copied state in new atomics. + knownValue := &atomic.Value{} + knownValue.Store(known) + enabledValue := &atomic.Value{} + enabledValue.Store(enabled) + + // Construct a new featureGate around the copied state. + // Note that specialFeatures is treated as immutable by convention, + // and we maintain the value of f.closed across the copy. + return &featureGate{ + special: specialFeatures, + known: knownValue, + enabled: enabledValue, + closed: f.closed, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 7e0191fb6..2f81097cd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1008,6 +1008,7 @@ k8s.io/code-generator/third_party/forked/golang/reflect ## explicit; go 1.16 k8s.io/component-base/config k8s.io/component-base/config/v1alpha1 +k8s.io/component-base/featuregate k8s.io/component-base/version k8s.io/component-base/version/verflag # k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c