Merge pull request #125052 from p0lyn0mial/upstream-client-go-env-var-set

client-go/features: add Set method to the envvar impl

Kubernetes-commit: 8991e825d5f2affce16a05666c11d7a2a773a628
This commit is contained in:
Kubernetes Publisher 2024-05-28 08:28:07 -07:00
commit 5f741a5d35
3 changed files with 173 additions and 37 deletions

View File

@ -47,6 +47,10 @@ var _ Gates = &envVarFeatureGates{}
// //
// Please note that environmental variables can only be set to the boolean value. // Please note that environmental variables can only be set to the boolean value.
// Incorrect values will be ignored and logged. // Incorrect values will be ignored and logged.
//
// Features can also be set directly via the Set method.
// In that case, these features take precedence over
// features set via environmental variables.
func newEnvVarFeatureGates(features map[Feature]FeatureSpec) *envVarFeatureGates { func newEnvVarFeatureGates(features map[Feature]FeatureSpec) *envVarFeatureGates {
known := map[Feature]FeatureSpec{} known := map[Feature]FeatureSpec{}
for name, spec := range features { for name, spec := range features {
@ -57,7 +61,8 @@ func newEnvVarFeatureGates(features map[Feature]FeatureSpec) *envVarFeatureGates
callSiteName: naming.GetNameFromCallsite(internalPackages...), callSiteName: naming.GetNameFromCallsite(internalPackages...),
known: known, known: known,
} }
fg.enabled.Store(map[Feature]bool{}) fg.enabledViaEnvVar.Store(map[Feature]bool{})
fg.enabledViaSetMethod = map[Feature]bool{}
return fg return fg
} }
@ -74,17 +79,34 @@ type envVarFeatureGates struct {
// known holds known feature gates // known holds known feature gates
known map[Feature]FeatureSpec known map[Feature]FeatureSpec
// enabled holds a map[Feature]bool // enabledViaEnvVar holds a map[Feature]bool
// with values explicitly set via env var // with values explicitly set via env var
enabled atomic.Value enabledViaEnvVar atomic.Value
// lockEnabledViaSetMethod protects enabledViaSetMethod
lockEnabledViaSetMethod sync.RWMutex
// enabledViaSetMethod holds values explicitly set
// via Set method, features stored in this map take
// precedence over features stored in enabledViaEnvVar
enabledViaSetMethod map[Feature]bool
// readEnvVars holds the boolean value which // readEnvVars holds the boolean value which
// indicates whether readEnvVarsOnce has been called. // indicates whether readEnvVarsOnce has been called.
readEnvVars atomic.Bool readEnvVars atomic.Bool
} }
// Enabled returns true if the key is enabled. If the key is not known, this call will panic. // 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 { func (f *envVarFeatureGates) Enabled(key Feature) bool {
if v, ok := f.wasFeatureEnabledViaSetMethod(key); ok {
// ensue that the state of all known features
// is loaded from environment variables
// on the first call to Enabled method.
if !f.hasAlreadyReadEnvVar() {
_ = f.getEnabledMapFromEnvVar()
}
return v
}
if v, ok := f.getEnabledMapFromEnvVar()[key]; ok { if v, ok := f.getEnabledMapFromEnvVar()[key]; ok {
return v return v
} }
@ -94,6 +116,26 @@ func (f *envVarFeatureGates) Enabled(key Feature) bool {
panic(fmt.Errorf("feature %q is not registered in FeatureGates %q", key, f.callSiteName)) panic(fmt.Errorf("feature %q is not registered in FeatureGates %q", key, f.callSiteName))
} }
// Set sets the given feature to the given value.
//
// Features set via this method take precedence over
// the features set via environment variables.
func (f *envVarFeatureGates) Set(featureName Feature, featureValue bool) error {
feature, ok := f.known[featureName]
if !ok {
return fmt.Errorf("feature %q is not registered in FeatureGates %q", featureName, f.callSiteName)
}
if feature.LockToDefault && feature.Default != featureValue {
return fmt.Errorf("cannot set feature gate %q to %v, feature is locked to %v", featureName, featureValue, feature.Default)
}
f.lockEnabledViaSetMethod.Lock()
defer f.lockEnabledViaSetMethod.Unlock()
f.enabledViaSetMethod[featureName] = featureValue
return nil
}
// getEnabledMapFromEnvVar will fill the enabled map on the first call. // getEnabledMapFromEnvVar will fill the enabled map on the first call.
// This is the only time a known feature can be set to a value // This is the only time a known feature can be set to a value
// read from the corresponding environmental variable. // read from the corresponding environmental variable.
@ -119,7 +161,7 @@ func (f *envVarFeatureGates) getEnabledMapFromEnvVar() map[Feature]bool {
featureGatesState[feature] = boolVal featureGatesState[feature] = boolVal
} }
} }
f.enabled.Store(featureGatesState) f.enabledViaEnvVar.Store(featureGatesState)
f.readEnvVars.Store(true) f.readEnvVars.Store(true)
for feature, featureSpec := range f.known { for feature, featureSpec := range f.known {
@ -130,7 +172,15 @@ func (f *envVarFeatureGates) getEnabledMapFromEnvVar() map[Feature]bool {
klog.V(1).InfoS("Feature gate default state", "feature", feature, "enabled", featureSpec.Default) klog.V(1).InfoS("Feature gate default state", "feature", feature, "enabled", featureSpec.Default)
} }
}) })
return f.enabled.Load().(map[Feature]bool) return f.enabledViaEnvVar.Load().(map[Feature]bool)
}
func (f *envVarFeatureGates) wasFeatureEnabledViaSetMethod(key Feature) (bool, bool) {
f.lockEnabledViaSetMethod.RLock()
defer f.lockEnabledViaSetMethod.RUnlock()
value, found := f.enabledViaSetMethod[key]
return value, found
} }
func (f *envVarFeatureGates) hasAlreadyReadEnvVar() bool { func (f *envVarFeatureGates) hasAlreadyReadEnvVar() bool {

View File

@ -23,21 +23,21 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestEnvVarFeatureGates(t *testing.T) { var defaultTestFeatures = map[Feature]FeatureSpec{
defaultTestFeatures := map[Feature]FeatureSpec{ "TestAlpha": {
"TestAlpha": { Default: false,
Default: false, LockToDefault: false,
LockToDefault: false, PreRelease: "Alpha",
PreRelease: "Alpha", },
}, "TestBeta": {
"TestBeta": { Default: true,
Default: true, LockToDefault: false,
LockToDefault: false, PreRelease: "Beta",
PreRelease: "Beta", },
}, }
}
expectedDefaultFeaturesState := map[Feature]bool{"TestAlpha": false, "TestBeta": true}
func TestEnvVarFeatureGates(t *testing.T) {
expectedDefaultFeaturesState := map[Feature]bool{"TestAlpha": false, "TestBeta": true}
copyExpectedStateMap := func(toCopy map[Feature]bool) map[Feature]bool { copyExpectedStateMap := func(toCopy map[Feature]bool) map[Feature]bool {
m := map[Feature]bool{} m := map[Feature]bool{}
for k, v := range toCopy { for k, v := range toCopy {
@ -47,11 +47,14 @@ func TestEnvVarFeatureGates(t *testing.T) {
} }
scenarios := []struct { scenarios := []struct {
name string name string
features map[Feature]FeatureSpec features map[Feature]FeatureSpec
envVariables map[string]string envVariables map[string]string
expectedFeaturesState map[Feature]bool setMethodFeatures map[Feature]bool
expectedInternalEnabledFeatureState map[Feature]bool
expectedFeaturesState map[Feature]bool
expectedInternalEnabledViaEnvVarFeatureState map[Feature]bool
expectedInternalEnabledViaSetMethodFeatureState map[Feature]bool
}{ }{
{ {
name: "can add empty features", name: "can add empty features",
@ -76,7 +79,7 @@ func TestEnvVarFeatureGates(t *testing.T) {
expectedDefaultFeaturesStateCopy["TestAlpha"] = true expectedDefaultFeaturesStateCopy["TestAlpha"] = true
return expectedDefaultFeaturesStateCopy return expectedDefaultFeaturesStateCopy
}(), }(),
expectedInternalEnabledFeatureState: map[Feature]bool{"TestAlpha": true}, expectedInternalEnabledViaEnvVarFeatureState: map[Feature]bool{"TestAlpha": true},
}, },
{ {
name: "incorrect env var value gets ignored", name: "incorrect env var value gets ignored",
@ -111,9 +114,25 @@ func TestEnvVarFeatureGates(t *testing.T) {
PreRelease: "Alpha", PreRelease: "Alpha",
}, },
}, },
envVariables: map[string]string{"KUBE_FEATURE_TestAlpha": "True"}, envVariables: map[string]string{"KUBE_FEATURE_TestAlpha": "True"},
expectedFeaturesState: map[Feature]bool{"TestAlpha": true}, expectedFeaturesState: map[Feature]bool{"TestAlpha": true},
expectedInternalEnabledFeatureState: map[Feature]bool{"TestAlpha": true}, expectedInternalEnabledViaEnvVarFeatureState: map[Feature]bool{"TestAlpha": true},
},
{
name: "setting a feature via the Set method works",
features: defaultTestFeatures,
setMethodFeatures: map[Feature]bool{"TestAlpha": true},
expectedFeaturesState: map[Feature]bool{"TestAlpha": true},
expectedInternalEnabledViaSetMethodFeatureState: map[Feature]bool{"TestAlpha": true},
},
{
name: "setting a feature via the Set method wins",
features: defaultTestFeatures,
setMethodFeatures: map[Feature]bool{"TestAlpha": false},
envVariables: map[string]string{"KUBE_FEATURE_TestAlpha": "True"},
expectedFeaturesState: map[Feature]bool{"TestAlpha": false},
expectedInternalEnabledViaEnvVarFeatureState: map[Feature]bool{"TestAlpha": true},
expectedInternalEnabledViaSetMethodFeatureState: map[Feature]bool{"TestAlpha": false},
}, },
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
@ -123,20 +142,33 @@ func TestEnvVarFeatureGates(t *testing.T) {
} }
target := newEnvVarFeatureGates(scenario.features) target := newEnvVarFeatureGates(scenario.features)
for k, v := range scenario.setMethodFeatures {
err := target.Set(k, v)
require.NoError(t, err)
}
for expectedFeature, expectedValue := range scenario.expectedFeaturesState { for expectedFeature, expectedValue := range scenario.expectedFeaturesState {
actualValue := target.Enabled(expectedFeature) actualValue := target.Enabled(expectedFeature)
require.Equal(t, actualValue, expectedValue, "expected feature=%v, to be=%v, not=%v", expectedFeature, expectedValue, actualValue) require.Equal(t, actualValue, expectedValue, "expected feature=%v, to be=%v, not=%v", expectedFeature, expectedValue, actualValue)
} }
enabledInternalMap := target.enabled.Load().(map[Feature]bool) enabledViaEnvVarInternalMap := target.enabledViaEnvVar.Load().(map[Feature]bool)
require.Len(t, enabledInternalMap, len(scenario.expectedInternalEnabledFeatureState)) require.Len(t, enabledViaEnvVarInternalMap, len(scenario.expectedInternalEnabledViaEnvVarFeatureState))
for expectedFeatureName, expectedFeatureValue := range scenario.expectedInternalEnabledViaEnvVarFeatureState {
actualFeatureValue, wasExpectedFeatureFound := enabledViaEnvVarInternalMap[expectedFeatureName]
if !wasExpectedFeatureFound {
t.Errorf("feature %v has not been found in enabledViaEnvVarInternalMap", expectedFeatureName)
}
require.Equal(t, expectedFeatureValue, actualFeatureValue, "feature %v has incorrect value = %v, expected = %v", expectedFeatureName, actualFeatureValue, expectedFeatureValue)
}
for expectedFeature, expectedInternalPresence := range scenario.expectedInternalEnabledFeatureState { enabledViaSetMethodInternalMap := target.enabledViaSetMethod
featureInternalValue, featureSet := enabledInternalMap[expectedFeature] require.Len(t, enabledViaSetMethodInternalMap, len(scenario.expectedInternalEnabledViaSetMethodFeatureState))
require.Equal(t, expectedInternalPresence, featureSet, "feature %v present = %v, expected = %v", expectedFeature, featureSet, expectedInternalPresence) for expectedFeatureName, expectedFeatureValue := range scenario.expectedInternalEnabledViaSetMethodFeatureState {
actualFeatureValue, wasExpectedFeatureFound := enabledViaSetMethodInternalMap[expectedFeatureName]
expectedFeatureInternalValue := scenario.expectedFeaturesState[expectedFeature] if !wasExpectedFeatureFound {
require.Equal(t, expectedFeatureInternalValue, featureInternalValue) t.Errorf("feature %v has not been found in enabledViaSetMethod", expectedFeatureName)
}
require.Equal(t, expectedFeatureValue, actualFeatureValue, "feature %v has incorrect value = %v, expected = %v", expectedFeatureName, actualFeatureValue, expectedFeatureValue)
} }
}) })
} }
@ -154,3 +186,48 @@ func TestHasAlreadyReadEnvVar(t *testing.T) {
_ = target.getEnabledMapFromEnvVar() _ = target.getEnabledMapFromEnvVar()
require.True(t, target.hasAlreadyReadEnvVar()) require.True(t, target.hasAlreadyReadEnvVar())
} }
func TestEnvVarFeatureGatesSetNegative(t *testing.T) {
scenarios := []struct {
name string
features map[Feature]FeatureSpec
featureName Feature
featureValue bool
expectedErr func(string) error
}{
{
name: "empty feature name returns an error",
features: defaultTestFeatures,
expectedErr: func(callSiteName string) error {
return fmt.Errorf("feature %q is not registered in FeatureGates %q", "", callSiteName)
},
},
{
name: "setting unknown feature returns an error",
features: defaultTestFeatures,
featureName: "Unknown",
expectedErr: func(callSiteName string) error {
return fmt.Errorf("feature %q is not registered in FeatureGates %q", "Unknown", callSiteName)
},
},
{
name: "setting locked feature returns an error",
features: map[Feature]FeatureSpec{"LockedFeature": {LockToDefault: true, Default: true}},
featureName: "LockedFeature",
featureValue: false,
expectedErr: func(_ string) error {
return fmt.Errorf("cannot set feature gate %q to %v, feature is locked to %v", "LockedFeature", false, true)
},
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
target := newEnvVarFeatureGates(scenario.features)
err := target.Set(scenario.featureName, scenario.featureValue)
require.Equal(t, scenario.expectedErr(target.callSiteName), err)
})
}
}

View File

@ -30,6 +30,15 @@ func TestAddFeaturesToExistingFeatureGates(t *testing.T) {
require.Equal(t, defaultKubernetesFeatureGates, fakeFeatureGates.specs) require.Equal(t, defaultKubernetesFeatureGates, fakeFeatureGates.specs)
} }
func TestReplaceFeatureGatesWithWarningIndicator(t *testing.T) {
defaultFeatureGates := FeatureGates()
require.Panics(t, func() { defaultFeatureGates.Enabled("Foo") }, "reading an unregistered feature gate Foo should panic")
if !replaceFeatureGatesWithWarningIndicator(defaultFeatureGates) {
t.Error("replacing the default feature gates after reading a value hasn't produced a warning")
}
}
type fakeRegistry struct { type fakeRegistry struct {
specs map[Feature]FeatureSpec specs map[Feature]FeatureSpec
} }