Merge pull request #122555 from p0lyn0mial/upstream-client-go-fg-provider-with-types
client-go/features: introduce feature gates
This commit is contained in:
		
							
								
								
									
										138
									
								
								staging/src/k8s.io/client-go/features/envvar.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								staging/src/k8s.io/client-go/features/envvar.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | ||||
| /* | ||||
| Copyright 2024 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 features | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/naming" | ||||
| 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||
| 	"k8s.io/klog/v2" | ||||
| ) | ||||
|  | ||||
| // 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/client-go/features/envvar.go"} | ||||
|  | ||||
| var _ Gates = &envVarFeatureGates{} | ||||
|  | ||||
| // newEnvVarFeatureGates creates a feature gate that allows for registration | ||||
| // of features and checking if the features are enabled. | ||||
| // | ||||
| // On the first call to Enabled, the effective state of all known features is loaded from | ||||
| // environment variables. The environment variable read for a given feature is formed by | ||||
| // concatenating the prefix "KUBE_FEATURE_" with the feature's name. | ||||
| // | ||||
| // For example, if you have a feature named "MyFeature" | ||||
| // setting an environmental variable "KUBE_FEATURE_MyFeature" | ||||
| // will allow you to configure the state of that feature. | ||||
| // | ||||
| // Please note that environmental variables can only be set to the boolean value. | ||||
| // Incorrect values will be ignored and logged. | ||||
| func newEnvVarFeatureGates(features map[Feature]FeatureSpec) *envVarFeatureGates { | ||||
| 	known := map[Feature]FeatureSpec{} | ||||
| 	for name, spec := range features { | ||||
| 		known[name] = spec | ||||
| 	} | ||||
|  | ||||
| 	fg := &envVarFeatureGates{ | ||||
| 		callSiteName: naming.GetNameFromCallsite(internalPackages...), | ||||
| 		known:        known, | ||||
| 	} | ||||
| 	fg.enabled.Store(map[Feature]bool{}) | ||||
|  | ||||
| 	return fg | ||||
| } | ||||
|  | ||||
| // envVarFeatureGates implements Gates and allows for feature registration. | ||||
| type envVarFeatureGates struct { | ||||
| 	// callSiteName holds the name of the file | ||||
| 	// that created this instance | ||||
| 	callSiteName string | ||||
|  | ||||
| 	// readEnvVarsOnce guards reading environmental variables | ||||
| 	readEnvVarsOnce sync.Once | ||||
|  | ||||
| 	// known holds known feature gates | ||||
| 	known map[Feature]FeatureSpec | ||||
|  | ||||
| 	// enabled holds a map[Feature]bool | ||||
| 	// with values explicitly set via env var | ||||
| 	enabled atomic.Value | ||||
|  | ||||
| 	// readEnvVars holds the boolean value which | ||||
| 	// indicates whether readEnvVarsOnce has been called. | ||||
| 	readEnvVars atomic.Bool | ||||
| } | ||||
|  | ||||
| // Enabled returns true if the key is enabled.  If the key is not known, this call will panic. | ||||
| func (f *envVarFeatureGates) Enabled(key Feature) bool { | ||||
| 	if v, ok := f.getEnabledMapFromEnvVar()[key]; ok { | ||||
| 		return v | ||||
| 	} | ||||
| 	if v, ok := f.known[key]; ok { | ||||
| 		return v.Default | ||||
| 	} | ||||
| 	panic(fmt.Errorf("feature %q is not registered in FeatureGates %q", key, f.callSiteName)) | ||||
| } | ||||
|  | ||||
| // getEnabledMapFromEnvVar will fill the enabled map on the first call. | ||||
| // This is the only time a known feature can be set to a value | ||||
| // read from the corresponding environmental variable. | ||||
| func (f *envVarFeatureGates) getEnabledMapFromEnvVar() map[Feature]bool { | ||||
| 	f.readEnvVarsOnce.Do(func() { | ||||
| 		featureGatesState := map[Feature]bool{} | ||||
| 		for feature, featureSpec := range f.known { | ||||
| 			featureState, featureStateSet := os.LookupEnv(fmt.Sprintf("KUBE_FEATURE_%s", feature)) | ||||
| 			if !featureStateSet { | ||||
| 				continue | ||||
| 			} | ||||
| 			boolVal, boolErr := strconv.ParseBool(featureState) | ||||
| 			switch { | ||||
| 			case boolErr != nil: | ||||
| 				utilruntime.HandleError(fmt.Errorf("cannot set feature gate %q to %q, due to %v", feature, featureState, boolErr)) | ||||
| 			case featureSpec.LockToDefault: | ||||
| 				if boolVal != featureSpec.Default { | ||||
| 					utilruntime.HandleError(fmt.Errorf("cannot set feature gate %q to %q, feature is locked to %v", feature, featureState, featureSpec.Default)) | ||||
| 					break | ||||
| 				} | ||||
| 				featureGatesState[feature] = featureSpec.Default | ||||
| 			default: | ||||
| 				featureGatesState[feature] = boolVal | ||||
| 			} | ||||
| 		} | ||||
| 		f.enabled.Store(featureGatesState) | ||||
| 		f.readEnvVars.Store(true) | ||||
|  | ||||
| 		for feature, featureSpec := range f.known { | ||||
| 			if featureState, ok := featureGatesState[feature]; ok { | ||||
| 				klog.V(1).InfoS("Feature gate updated state", "feature", feature, "enabled", featureState) | ||||
| 				continue | ||||
| 			} | ||||
| 			klog.V(1).InfoS("Feature gate default state", "feature", feature, "enabled", featureSpec.Default) | ||||
| 		} | ||||
| 	}) | ||||
| 	return f.enabled.Load().(map[Feature]bool) | ||||
| } | ||||
|  | ||||
| func (f *envVarFeatureGates) hasAlreadyReadEnvVar() bool { | ||||
| 	return f.readEnvVars.Load() | ||||
| } | ||||
							
								
								
									
										156
									
								
								staging/src/k8s.io/client-go/features/envvar_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								staging/src/k8s.io/client-go/features/envvar_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| /* | ||||
| Copyright 2024 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 features | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestEnvVarFeatureGates(t *testing.T) { | ||||
| 	defaultTestFeatures := map[Feature]FeatureSpec{ | ||||
| 		"TestAlpha": { | ||||
| 			Default:       false, | ||||
| 			LockToDefault: false, | ||||
| 			PreRelease:    "Alpha", | ||||
| 		}, | ||||
| 		"TestBeta": { | ||||
| 			Default:       true, | ||||
| 			LockToDefault: false, | ||||
| 			PreRelease:    "Beta", | ||||
| 		}, | ||||
| 	} | ||||
| 	expectedDefaultFeaturesState := map[Feature]bool{"TestAlpha": false, "TestBeta": true} | ||||
|  | ||||
| 	copyExpectedStateMap := func(toCopy map[Feature]bool) map[Feature]bool { | ||||
| 		m := map[Feature]bool{} | ||||
| 		for k, v := range toCopy { | ||||
| 			m[k] = v | ||||
| 		} | ||||
| 		return m | ||||
| 	} | ||||
|  | ||||
| 	scenarios := []struct { | ||||
| 		name                                string | ||||
| 		features                            map[Feature]FeatureSpec | ||||
| 		envVariables                        map[string]string | ||||
| 		expectedFeaturesState               map[Feature]bool | ||||
| 		expectedInternalEnabledFeatureState map[Feature]bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "can add empty features", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                  "no env var, features get Defaults assigned", | ||||
| 			features:              defaultTestFeatures, | ||||
| 			expectedFeaturesState: expectedDefaultFeaturesState, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                  "incorrect env var, feature gets Default assigned", | ||||
| 			features:              defaultTestFeatures, | ||||
| 			envVariables:          map[string]string{"TestAlpha": "true"}, | ||||
| 			expectedFeaturesState: expectedDefaultFeaturesState, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:         "correct env var changes the feature gets state", | ||||
| 			features:     defaultTestFeatures, | ||||
| 			envVariables: map[string]string{"KUBE_FEATURE_TestAlpha": "true"}, | ||||
| 			expectedFeaturesState: func() map[Feature]bool { | ||||
| 				expectedDefaultFeaturesStateCopy := copyExpectedStateMap(expectedDefaultFeaturesState) | ||||
| 				expectedDefaultFeaturesStateCopy["TestAlpha"] = true | ||||
| 				return expectedDefaultFeaturesStateCopy | ||||
| 			}(), | ||||
| 			expectedInternalEnabledFeatureState: map[Feature]bool{"TestAlpha": true}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                  "incorrect env var value gets ignored", | ||||
| 			features:              defaultTestFeatures, | ||||
| 			envVariables:          map[string]string{"KUBE_FEATURE_TestAlpha": "TrueFalse"}, | ||||
| 			expectedFeaturesState: expectedDefaultFeaturesState, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                  "empty env var value gets ignored", | ||||
| 			features:              defaultTestFeatures, | ||||
| 			envVariables:          map[string]string{"KUBE_FEATURE_TestAlpha": ""}, | ||||
| 			expectedFeaturesState: expectedDefaultFeaturesState, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "a feature LockToDefault wins", | ||||
| 			features: map[Feature]FeatureSpec{ | ||||
| 				"TestAlpha": { | ||||
| 					Default:       true, | ||||
| 					LockToDefault: true, | ||||
| 					PreRelease:    "Alpha", | ||||
| 				}, | ||||
| 			}, | ||||
| 			envVariables:          map[string]string{"KUBE_FEATURE_TestAlpha": "False"}, | ||||
| 			expectedFeaturesState: map[Feature]bool{"TestAlpha": true}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "setting a feature to LockToDefault changes the internal state", | ||||
| 			features: map[Feature]FeatureSpec{ | ||||
| 				"TestAlpha": { | ||||
| 					Default:       true, | ||||
| 					LockToDefault: true, | ||||
| 					PreRelease:    "Alpha", | ||||
| 				}, | ||||
| 			}, | ||||
| 			envVariables:                        map[string]string{"KUBE_FEATURE_TestAlpha": "True"}, | ||||
| 			expectedFeaturesState:               map[Feature]bool{"TestAlpha": true}, | ||||
| 			expectedInternalEnabledFeatureState: map[Feature]bool{"TestAlpha": true}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, scenario := range scenarios { | ||||
| 		t.Run(scenario.name, func(t *testing.T) { | ||||
| 			for k, v := range scenario.envVariables { | ||||
| 				t.Setenv(k, v) | ||||
| 			} | ||||
| 			target := newEnvVarFeatureGates(scenario.features) | ||||
|  | ||||
| 			for expectedFeature, expectedValue := range scenario.expectedFeaturesState { | ||||
| 				actualValue := target.Enabled(expectedFeature) | ||||
| 				require.Equal(t, actualValue, expectedValue, "expected feature=%v, to be=%v, not=%v", expectedFeature, expectedValue, actualValue) | ||||
| 			} | ||||
|  | ||||
| 			enabledInternalMap := target.enabled.Load().(map[Feature]bool) | ||||
| 			require.Len(t, enabledInternalMap, len(scenario.expectedInternalEnabledFeatureState)) | ||||
|  | ||||
| 			for expectedFeature, expectedInternalPresence := range scenario.expectedInternalEnabledFeatureState { | ||||
| 				featureInternalValue, featureSet := enabledInternalMap[expectedFeature] | ||||
| 				require.Equal(t, expectedInternalPresence, featureSet, "feature %v present = %v, expected = %v", expectedFeature, featureSet, expectedInternalPresence) | ||||
|  | ||||
| 				expectedFeatureInternalValue := scenario.expectedFeaturesState[expectedFeature] | ||||
| 				require.Equal(t, expectedFeatureInternalValue, featureInternalValue) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestEnvVarFeatureGatesEnabledPanic(t *testing.T) { | ||||
| 	target := newEnvVarFeatureGates(nil) | ||||
| 	require.PanicsWithError(t, fmt.Errorf("feature %q is not registered in FeatureGates %q", "UnknownFeature", target.callSiteName).Error(), func() { target.Enabled("UnknownFeature") }) | ||||
| } | ||||
|  | ||||
| func TestHasAlreadyReadEnvVar(t *testing.T) { | ||||
| 	target := newEnvVarFeatureGates(nil) | ||||
| 	require.False(t, target.hasAlreadyReadEnvVar()) | ||||
|  | ||||
| 	_ = target.getEnabledMapFromEnvVar() | ||||
| 	require.True(t, target.hasAlreadyReadEnvVar()) | ||||
| } | ||||
							
								
								
									
										149
									
								
								staging/src/k8s.io/client-go/features/features.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								staging/src/k8s.io/client-go/features/features.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| /* | ||||
| Copyright 2024 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 features | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||
| 	"sync/atomic" | ||||
| ) | ||||
|  | ||||
| // NOTE: types Feature, FeatureSpec, prerelease (and its values) | ||||
| // were duplicated from the component-base repository | ||||
| // | ||||
| // for more information please refer to https://docs.google.com/document/d/1g9BGCRw-7ucUxO6OtCWbb3lfzUGA_uU9178wLdXAIfs | ||||
|  | ||||
| const ( | ||||
| 	// Values for PreRelease. | ||||
| 	Alpha = prerelease("ALPHA") | ||||
| 	Beta  = prerelease("BETA") | ||||
| 	GA    = prerelease("") | ||||
|  | ||||
| 	// Deprecated | ||||
| 	Deprecated = prerelease("DEPRECATED") | ||||
| ) | ||||
|  | ||||
| type prerelease string | ||||
|  | ||||
| type Feature string | ||||
|  | ||||
| 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 | ||||
| } | ||||
|  | ||||
| // Gates indicates whether a given feature is enabled or not. | ||||
| type Gates interface { | ||||
| 	// Enabled returns true if the key is enabled. | ||||
| 	Enabled(key Feature) bool | ||||
| } | ||||
|  | ||||
| // Registry represents an external feature gates registry. | ||||
| type Registry interface { | ||||
| 	// Add adds existing feature gates to the provided registry. | ||||
| 	// | ||||
| 	// As of today, this method is used by AddFeaturesToExistingFeatureGates and | ||||
| 	// ReplaceFeatureGates to take control of the features exposed by this library. | ||||
| 	Add(map[Feature]FeatureSpec) error | ||||
| } | ||||
|  | ||||
| // FeatureGates returns the feature gates exposed by this library. | ||||
| // | ||||
| // By default, only the default features gates will be returned. | ||||
| // The default implementation allows controlling the features | ||||
| // via environmental variables. | ||||
| // For example, if you have a feature named "MyFeature" | ||||
| // setting an environmental variable "KUBE_FEATURE_MyFeature" | ||||
| // will allow you to configure the state of that feature. | ||||
| // | ||||
| // Please note that the actual set of the feature gates | ||||
| // might be overwritten by calling ReplaceFeatureGates method. | ||||
| func FeatureGates() Gates { | ||||
| 	return featureGates.Load().(*featureGatesWrapper).Gates | ||||
| } | ||||
|  | ||||
| // AddFeaturesToExistingFeatureGates adds the default feature gates to the provided registry. | ||||
| // Usually this function is combined with ReplaceFeatureGates to take control of the | ||||
| // features exposed by this library. | ||||
| func AddFeaturesToExistingFeatureGates(registry Registry) error { | ||||
| 	return registry.Add(defaultKubernetesFeatureGates) | ||||
| } | ||||
|  | ||||
| // ReplaceFeatureGates overwrites the default implementation of the feature gates | ||||
| // used by this library. | ||||
| // | ||||
| // Useful for binaries that would like to have full control of the features | ||||
| // exposed by this library, such as allowing consumers of a binary | ||||
| // to interact with the features via a command line flag. | ||||
| // | ||||
| // For example: | ||||
| // | ||||
| //	// first, register client-go's features to your registry. | ||||
| //	clientgofeaturegate.AddFeaturesToExistingFeatureGates(utilfeature.DefaultMutableFeatureGate) | ||||
| //	// then replace client-go's feature gates implementation with your implementation | ||||
| //	clientgofeaturegate.ReplaceFeatureGates(utilfeature.DefaultMutableFeatureGate) | ||||
| func ReplaceFeatureGates(newFeatureGates Gates) { | ||||
| 	if replaceFeatureGatesWithWarningIndicator(newFeatureGates) { | ||||
| 		utilruntime.HandleError(errors.New("the default feature gates implementation has already been used and now it's being overwritten. This might lead to unexpected behaviour. Check your initialization order")) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func replaceFeatureGatesWithWarningIndicator(newFeatureGates Gates) bool { | ||||
| 	shouldProduceWarning := false | ||||
|  | ||||
| 	if defaultFeatureGates, ok := FeatureGates().(*envVarFeatureGates); ok { | ||||
| 		if defaultFeatureGates.hasAlreadyReadEnvVar() { | ||||
| 			shouldProduceWarning = true | ||||
| 		} | ||||
| 	} | ||||
| 	wrappedFeatureGates := &featureGatesWrapper{newFeatureGates} | ||||
| 	featureGates.Store(wrappedFeatureGates) | ||||
|  | ||||
| 	return shouldProduceWarning | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	envVarGates := newEnvVarFeatureGates(defaultKubernetesFeatureGates) | ||||
|  | ||||
| 	wrappedFeatureGates := &featureGatesWrapper{envVarGates} | ||||
| 	featureGates.Store(wrappedFeatureGates) | ||||
| } | ||||
|  | ||||
| // featureGatesWrapper a thin wrapper to satisfy featureGates variable (atomic.Value). | ||||
| // That is, all calls to Store for a given Value must use values of the same concrete type. | ||||
| type featureGatesWrapper struct { | ||||
| 	Gates | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	// featureGates is a shared global FeatureGates. | ||||
| 	// | ||||
| 	// Top-level commands/options setup that needs to modify this feature gates | ||||
| 	// should use AddFeaturesToExistingFeatureGates followed by ReplaceFeatureGates. | ||||
| 	featureGates = &atomic.Value{} | ||||
| ) | ||||
|  | ||||
| // defaultKubernetesFeatureGates consists of all known Kubernetes-specific feature keys. | ||||
| // | ||||
| // To add a new feature, define a key for it above and add it here. The features will be | ||||
| // available throughout Kubernetes binaries. | ||||
| var defaultKubernetesFeatureGates = map[Feature]FeatureSpec{} | ||||
							
								
								
									
										40
									
								
								staging/src/k8s.io/client-go/features/features_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								staging/src/k8s.io/client-go/features/features_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| /* | ||||
| Copyright 2024 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 features | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| // TestAddFeaturesToExistingFeatureGates ensures that | ||||
| // the defaultKubernetesFeatureGates are added to a test feature gates registry. | ||||
| func TestAddFeaturesToExistingFeatureGates(t *testing.T) { | ||||
| 	fakeFeatureGates := &fakeRegistry{} | ||||
| 	require.NoError(t, AddFeaturesToExistingFeatureGates(fakeFeatureGates)) | ||||
| 	require.Equal(t, defaultKubernetesFeatureGates, fakeFeatureGates.specs) | ||||
| } | ||||
|  | ||||
| type fakeRegistry struct { | ||||
| 	specs map[Feature]FeatureSpec | ||||
| } | ||||
|  | ||||
| func (f *fakeRegistry) Add(specs map[Feature]FeatureSpec) error { | ||||
| 	f.specs = specs | ||||
| 	return nil | ||||
| } | ||||
| @@ -0,0 +1,79 @@ | ||||
| /* | ||||
| Copyright 2024 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 testing | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
|  | ||||
| 	"k8s.io/client-go/features" | ||||
| ) | ||||
|  | ||||
| func TestDriveInitDefaultFeatureGates(t *testing.T) { | ||||
| 	featureGates := features.FeatureGates() | ||||
| 	assertFunctionPanicsWithMessage(t, func() { featureGates.Enabled("FakeFeatureGate") }, "features.FeatureGates().Enabled", fmt.Sprintf("feature %q is not registered in FeatureGate", "FakeFeatureGate")) | ||||
|  | ||||
| 	fakeFeatureGates := &alwaysEnabledFakeGates{} | ||||
| 	require.True(t, fakeFeatureGates.Enabled("FakeFeatureGate")) | ||||
|  | ||||
| 	features.ReplaceFeatureGates(fakeFeatureGates) | ||||
| 	featureGates = features.FeatureGates() | ||||
|  | ||||
| 	assertFeatureGatesType(t, featureGates) | ||||
| 	require.True(t, featureGates.Enabled("FakeFeatureGate")) | ||||
| } | ||||
|  | ||||
| type alwaysEnabledFakeGates struct{} | ||||
|  | ||||
| func (f *alwaysEnabledFakeGates) Enabled(features.Feature) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func assertFeatureGatesType(t *testing.T, fg features.Gates) { | ||||
| 	_, ok := fg.(*alwaysEnabledFakeGates) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("passed features.FeatureGates() is NOT of type *alwaysEnabledFakeGates, it is of type = %T", fg) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func assertFunctionPanicsWithMessage(t *testing.T, f func(), fName, errMessage string) { | ||||
| 	didPanic, panicMessage := didFunctionPanic(f) | ||||
| 	if !didPanic { | ||||
| 		t.Fatalf("function %q did not panicked", fName) | ||||
| 	} | ||||
|  | ||||
| 	panicError, ok := panicMessage.(error) | ||||
| 	if !ok || !strings.Contains(panicError.Error(), errMessage) { | ||||
| 		t.Fatalf("func %q should panic with error message:\t%#v\n\tPanic value:\t%#v\n", fName, errMessage, panicMessage) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func didFunctionPanic(f func()) (didPanic bool, panicMessage interface{}) { | ||||
| 	didPanic = true | ||||
|  | ||||
| 	defer func() { | ||||
| 		panicMessage = recover() | ||||
| 	}() | ||||
|  | ||||
| 	f() | ||||
| 	didPanic = false | ||||
|  | ||||
| 	return | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot