Merge pull request #31140 from thockin/feature-gate-fixes

Automatic merge from submit-queue

Make 'allAlpha' a special feature gate

Rather than making all caller check both allAlpha and their own flag, make `allAlpha` set all of the alpha gates explicitly.

This is hard to test because of the globalness.  I will follow this commit with a new one to add some way to test, but I wanted to float this design
This commit is contained in:
Kubernetes Submit Queue 2016-08-22 21:06:29 -07:00 committed by GitHub
commit 25ce84b34e
2 changed files with 185 additions and 52 deletions

View File

@ -31,48 +31,78 @@ const (
// All known feature keys // All known feature keys
// To add a new feature, define a key for it below and add // To add a new feature, define a key for it below and add
// a featureInfo entry to knownFeatures. // a featureSpec entry to knownFeatures.
// allAlpha is a global toggle for alpha features. Per feature key // allAlphaGate is a global toggle for alpha features. Per-feature key
// values override the default set by allAlpha. // values override the default set by allAlphaGate, if they come later in the
// e.g. allAlpha=false,newFeature=true will result in newFeature=true // specification of gates. Examples:
allAlpha = "allAlpha" // AllAlpha=false,NewFeature=true will result in newFeature=true
// AllAlpha=true,NewFeature=false will result in newFeature=false
allAlphaGate = "AllAlpha"
) )
var ( var (
// DefaultFeatureGate is a shared global FeatureGate. // Default values for recorded features. Every new feature gate should be
DefaultFeatureGate = &featureGate{} // represented here.
knownFeatures = map[string]featureSpec{
// Default values for recorded features. allAlphaGate: {false, alpha},
knownFeatures = map[string]featureInfo{
allAlpha: {false, alpha},
} }
// Special handling for a few gates.
specialFeatures = map[string]func(f *featureGate, val bool){
allAlphaGate: setUnsetAlphaGates,
}
// DefaultFeatureGate is a shared global FeatureGate.
DefaultFeatureGate = &featureGate{
known: knownFeatures,
special: specialFeatures,
}
)
type featureSpec struct {
enabled bool
prerelease prerelease
}
type prerelease string
const (
// Values for prerelease.
alpha = prerelease("ALPHA") alpha = prerelease("ALPHA")
beta = prerelease("BETA") beta = prerelease("BETA")
ga = prerelease("") ga = prerelease("")
) )
type prerelease string
type featureInfo struct {
enabled bool
prerelease prerelease
}
// FeatureGate parses and stores flag gates for known features from // FeatureGate parses and stores flag gates for known features from
// a string like feature1=true,feature2=false,... // a string like feature1=true,feature2=false,...
type FeatureGate interface { type FeatureGate interface {
AddFlag(fs *pflag.FlagSet) AddFlag(fs *pflag.FlagSet)
// owner: @jlowdermilk
// alpha: v1.4 // Every feature gate should add method here following this template:
AllAlpha() bool //
// // owner: @username
// // alpha: v1.4
// MyFeature() bool
// TODO: Define accessors for each non-API alpha feature. // TODO: Define accessors for each non-API alpha feature.
} }
// featureGate implements FeatureGate as well as pflag.Value for flag parsing. // featureGate implements FeatureGate as well as pflag.Value for flag parsing.
type featureGate struct { type featureGate struct {
features map[string]bool known map[string]featureSpec
special map[string]func(*featureGate, bool)
enabled map[string]bool
}
func setUnsetAlphaGates(f *featureGate, val bool) {
for k, v := range f.known {
if v.prerelease == alpha {
if _, found := f.enabled[k]; !found {
f.enabled[k] = val
}
}
}
} }
// Set, String, and Type implement pflag.Value // Set, String, and Type implement pflag.Value
@ -80,14 +110,14 @@ type featureGate struct {
// Set Parses a string of the form // "key1=value1,key2=value2,..." into a // Set Parses a string of the form // "key1=value1,key2=value2,..." into a
// map[string]bool of known keys or returns an error. // map[string]bool of known keys or returns an error.
func (f *featureGate) Set(value string) error { func (f *featureGate) Set(value string) error {
f.features = make(map[string]bool) f.enabled = make(map[string]bool)
for _, s := range strings.Split(value, ",") { for _, s := range strings.Split(value, ",") {
if len(s) == 0 { if len(s) == 0 {
continue continue
} }
arr := strings.SplitN(s, "=", 2) arr := strings.SplitN(s, "=", 2)
k := strings.TrimSpace(arr[0]) k := strings.TrimSpace(arr[0])
_, ok := knownFeatures[k] _, ok := f.known[k]
if !ok { if !ok {
return fmt.Errorf("unrecognized key: %s", k) return fmt.Errorf("unrecognized key: %s", k)
} }
@ -99,15 +129,21 @@ func (f *featureGate) Set(value string) error {
if err != nil { if err != nil {
return fmt.Errorf("invalid value of %s: %s, err: %v", k, v, err) return fmt.Errorf("invalid value of %s: %s, err: %v", k, v, err)
} }
f.features[k] = boolValue f.enabled[k] = boolValue
// Handle "special" features like "all alpha gates"
if fn, found := f.special[k]; found {
fn(f, boolValue)
}
} }
glog.Infof("feature gates: %v", f.features)
glog.Infof("feature gates: %v", f.enabled)
return nil return nil
} }
func (f *featureGate) String() string { func (f *featureGate) String() string {
pairs := []string{} pairs := []string{}
for k, v := range f.features { for k, v := range f.enabled {
pairs = append(pairs, fmt.Sprintf("%s=%t", k, v)) pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
} }
sort.Strings(pairs) sort.Strings(pairs)
@ -118,18 +154,12 @@ func (f *featureGate) Type() string {
return "mapStringBool" return "mapStringBool"
} }
// AllAlpha returns value for allAlpha.
func (f *featureGate) AllAlpha() bool {
return f.lookup(allAlpha)
}
func (f *featureGate) lookup(key string) bool { func (f *featureGate) lookup(key string) bool {
defaultValue := knownFeatures[key].enabled defaultValue := f.known[key].enabled
if f.features == nil { if f.enabled != nil {
panic(fmt.Sprintf("--%s has not been parsed", flagName)) if v, ok := f.enabled[key]; ok {
} return v
if v, ok := f.features[key]; ok { }
return v
} }
return defaultValue return defaultValue
@ -138,7 +168,7 @@ func (f *featureGate) lookup(key string) bool {
// AddFlag adds a flag for setting global feature gates to the specified FlagSet. // AddFlag adds a flag for setting global feature gates to the specified FlagSet.
func (f *featureGate) AddFlag(fs *pflag.FlagSet) { func (f *featureGate) AddFlag(fs *pflag.FlagSet) {
var known []string var known []string
for k, v := range knownFeatures { for k, v := range f.known {
pre := "" pre := ""
if v.prerelease != ga { if v.prerelease != ga {
pre = fmt.Sprintf("%s - ", v.prerelease) pre = fmt.Sprintf("%s - ", v.prerelease)

View File

@ -25,32 +25,135 @@ import (
) )
func TestFeatureGateFlag(t *testing.T) { func TestFeatureGateFlag(t *testing.T) {
// gates for testing
const testAlphaGate = "TestAlpha"
const testBetaGate = "TestBeta"
tests := []struct { tests := []struct {
arg string arg string
allAlpha bool expect map[string]bool
parseError error parseError string
}{ }{
{fmt.Sprintf("--%s=fooBarBaz=maybeidk", flagName), false, fmt.Errorf("unrecognized key: fooBarBaz")}, {
{fmt.Sprintf("--%s=", flagName), false, nil}, arg: "",
{fmt.Sprintf("--%s=allAlpha=false", flagName), false, nil}, expect: map[string]bool{
{fmt.Sprintf("--%s=allAlpha=true", flagName), true, nil}, allAlphaGate: false,
{fmt.Sprintf("--%s=allAlpha=banana", flagName), false, fmt.Errorf("invalid value of allAlpha")}, testAlphaGate: false,
testBetaGate: false,
},
},
{
arg: "fooBarBaz=maybeidk",
expect: map[string]bool{
allAlphaGate: false,
testAlphaGate: false,
testBetaGate: false,
},
parseError: "unrecognized key: fooBarBaz",
},
{
arg: "AllAlpha=false",
expect: map[string]bool{
allAlphaGate: false,
testAlphaGate: false,
testBetaGate: false,
},
},
{
arg: "AllAlpha=true",
expect: map[string]bool{
allAlphaGate: true,
testAlphaGate: true,
testBetaGate: false,
},
},
{
arg: "AllAlpha=banana",
expect: map[string]bool{
allAlphaGate: false,
testAlphaGate: false,
testBetaGate: false,
},
parseError: "invalid value of AllAlpha",
},
{
arg: "AllAlpha=false,TestAlpha=true",
expect: map[string]bool{
allAlphaGate: false,
testAlphaGate: true,
testBetaGate: false,
},
},
{
arg: "TestAlpha=true,AllAlpha=false",
expect: map[string]bool{
allAlphaGate: false,
testAlphaGate: true,
testBetaGate: false,
},
},
{
arg: "AllAlpha=true,TestAlpha=false",
expect: map[string]bool{
allAlphaGate: true,
testAlphaGate: false,
testBetaGate: false,
},
},
{
arg: "TestAlpha=false,AllAlpha=true",
expect: map[string]bool{
allAlphaGate: true,
testAlphaGate: false,
testBetaGate: false,
},
},
{
arg: "TestBeta=true,AllAlpha=false",
expect: map[string]bool{
allAlphaGate: false,
testAlphaGate: false,
testBetaGate: true,
},
},
} }
for i, test := range tests { for i, test := range tests {
fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError) fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError)
f := &featureGate{} f := DefaultFeatureGate
f.known[testAlphaGate] = featureSpec{false, alpha}
f.known[testBetaGate] = featureSpec{false, beta}
f.AddFlag(fs) f.AddFlag(fs)
err := fs.Parse([]string{test.arg}) err := fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, test.arg)})
if test.parseError != nil { if test.parseError != "" {
if !strings.Contains(err.Error(), test.parseError.Error()) { if !strings.Contains(err.Error(), test.parseError) {
t.Errorf("%d: Parse() Expected %v, Got %v", i, test.parseError, err) t.Errorf("%d: Parse() Expected %v, Got %v", i, test.parseError, err)
} }
} else if err != nil { } else if err != nil {
t.Errorf("%d: Parse() Expected nil, Got %v", i, err) t.Errorf("%d: Parse() Expected nil, Got %v", i, err)
} }
if alpha := f.AllAlpha(); alpha != test.allAlpha { for k, v := range test.expect {
t.Errorf("%d: AlphaEnabled() expected %v, Got %v", i, test.allAlpha, alpha) if f.enabled[k] != v {
t.Errorf("%d: expected %s=%v, Got %v", i, k, v, f.enabled[k])
}
} }
} }
} }
func TestFeatureGateFlagDefaults(t *testing.T) {
// gates for testing
const testAlphaGate = "TestAlpha"
const testBetaGate = "TestBeta"
// Don't parse the flag, assert defaults are used.
f := DefaultFeatureGate
f.known[testAlphaGate] = featureSpec{false, alpha}
f.known[testBetaGate] = featureSpec{true, beta}
if f.lookup(testAlphaGate) != false {
t.Errorf("Expected false")
}
if f.lookup(testBetaGate) != true {
t.Errorf("Expected true")
}
}