Skip to content

Commit f2920cd

Browse files
committed
HACK, UNTESTED: suppert external gate providers
1 parent 25912d0 commit f2920cd

File tree

3 files changed

+171
-54
lines changed

3 files changed

+171
-54
lines changed

gate_example.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/pflag"
7+
"k8s.io/utils/feature"
8+
)
9+
10+
var (
11+
libGates = &feature.GateSet{}
12+
f1 = libGates.Add(&feature.Gate{
13+
Name: "Feature1",
14+
Default: false,
15+
Release: feature.Alpha,
16+
})
17+
f2 = libGates.Add(&feature.Gate{
18+
Name: "Feature2",
19+
Default: true,
20+
Release: feature.Beta,
21+
})
22+
f3 = libGates.Add(&feature.Gate{
23+
Name: "Feature3",
24+
Default: true,
25+
Release: feature.GA,
26+
LockToDefault: true,
27+
})
28+
f4 = &feature.Gate{
29+
Name: "Feature4",
30+
Default: true,
31+
Release: feature.Beta,
32+
}
33+
)
34+
35+
func main() {
36+
libGates.EnablePFlagControl("gates", pflag.CommandLine)
37+
libGates.EnableEnvControl("GATE_")
38+
pflag.Parse()
39+
fmt.Println(feature.Enabled(f1))
40+
fmt.Println(feature.Enabled(f2))
41+
fmt.Println(feature.Enabled(f3))
42+
fmt.Println(feature.Enabled(f4))
43+
}

staging/src/k8s.io/apiserver/pkg/server/options/server_run_options.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,5 +340,6 @@ func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) {
340340
"This option, if set, represents the maximum amount of grace period the apiserver will wait "+
341341
"for active watch request(s) to drain during the graceful server shutdown window.")
342342

343-
s.FeatureGates.AddFlags("new-feature-gates", fs)
343+
s.FeatureGates.EnableFlagControl("new-feature-gates", fs)
344+
s.FeatureGates.EnableEnvControl("KUBE_FEATURE_")
344345
}

staging/src/k8s.io/utils/feature/feature.go

Lines changed: 126 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"sort"
2525
"strconv"
2626
"strings"
27-
"sync"
2827

2928
"github.com/spf13/pflag"
3029
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -36,7 +35,8 @@ import (
3635
// GateSet is a list of feature gates. All Gates in a GateSet must have unique
3736
// names.
3837
type GateSet struct {
39-
gateMap map[string]*Gate
38+
gateMap map[string]*Gate
39+
providers []GateProvider
4040
}
4141

4242
// Add adds the specified Gate to the GateSet.
@@ -54,16 +54,23 @@ func (gs *GateSet) Add(gate *Gate) *Gate {
5454
//FIXME: what should we do?
5555
}
5656
gs.gateMap[gate.Name] = gate
57+
gate.set = gs
5758
return gate
5859
}
5960

60-
// Merge adds all of the Gates in other to this GateSet.
61+
// Merge moves all of the Gates in other to this GateSet.
6162
func (gs *GateSet) Merge(other *GateSet) {
6263
for _, v := range other.gateMap {
6364
gs.Add(v)
65+
//FIXME: remove from other?
6466
}
6567
}
6668

69+
type GateProvider interface {
70+
//FIXME: comments, (val, found) return
71+
Get(name string) (bool, bool)
72+
}
73+
6774
const (
6875
// allAlphaGate is a global toggle for alpha features. Per-feature key
6976
// values override the default set by allAlphaGate. Examples:
@@ -78,12 +85,11 @@ const (
7885
allBetaGate = "AllBeta"
7986
)
8087

81-
// AddFlags adds gate-related flags to the specified FlagSet.
82-
// FIXME: how are gates exposed today?
83-
func (gs *GateSet) AddFlags(flagName string, fs *pflag.FlagSet) {
84-
for _, gate := range gs.gateMap {
85-
gate.readEnvVar()
86-
}
88+
// FIXME: comments
89+
// FIXME: We could move all the flag and env stuff out of this lib, and make
90+
// FIXME: callers do it, but it's considerably less useful.
91+
// FIXME: merge with existing gates flag and config
92+
func (gs *GateSet) EnablePFlagControl(flagName string, fs *pflag.FlagSet) {
8793
helpStrings := []string{
8894
fmt.Sprintf("%s=true|false (%s - default=%t)", allAlphaGate, Alpha, false),
8995
fmt.Sprintf("%s=true|false (%s - default=%t)", allBetaGate, Beta, false),
@@ -94,14 +100,31 @@ func (gs *GateSet) AddFlags(flagName string, fs *pflag.FlagSet) {
94100
sort.Strings(helpStrings)
95101

96102
//FIXME: nicer to have a --list-feature-gates flag?
97-
fs.Var(&flagValue{gs}, flagName, ""+
103+
fv := &flagValue{gates: gs}
104+
fs.Var(fv, flagName, ""+
98105
"A set of key=value pairs that describe feature gates for alpha/experimental features. "+
99106
"Options are:\n"+strings.Join(helpStrings, "\n"))
107+
108+
gs.providers = append(gs.providers, fv)
100109
}
101110

102-
// flagValue implements pflag.Value in terms of a GateSet.
111+
// flagValue implements pflag.Value and GateProvider.
103112
type flagValue struct {
104-
gates *GateSet
113+
gates *GateSet
114+
values map[string]bool
115+
}
116+
117+
// Implement GateProvider.
118+
func (fv *flagValue) Get(name string) (bool, bool) {
119+
val, found := fv.values[name]
120+
return val, found
121+
}
122+
123+
func (fv *flagValue) setOne(k string, v bool) {
124+
if fv.values == nil {
125+
fv.values = map[string]bool{}
126+
}
127+
fv.values[k] = v
105128
}
106129

107130
// Set parses a string of the form "key1=value1,key2=value2,..." into a
@@ -124,43 +147,42 @@ func (fv *flagValue) Set(value string) error {
124147
}
125148
m[k] = boolValue
126149
}
127-
return fv.setFromMap(m)
128-
}
129150

130-
func (fv *flagValue) setFromMap(m map[string]bool) error {
131151
for name, enabled := range m {
132152
switch {
133153
case name == allAlphaGate:
134154
for _, gate := range fv.gates.gateMap {
135155
if gate.Release == Alpha {
136-
gate.Set(enabled)
156+
fv.setOne(gate.Name, enabled)
137157
}
138158
}
139159
continue
140160
case name == allBetaGate:
141161
for _, gate := range fv.gates.gateMap {
142162
if gate.Release == Beta {
143-
gate.Set(enabled)
163+
fv.setOne(gate.Name, enabled)
144164
}
145165
}
146166
continue
147167
}
148168

169+
// FIXME: do we need this? Env won't fail with error unless we scan all vars
149170
gate, exists := fv.gates.gateMap[name]
150171
if !exists {
151172
return fmt.Errorf("unrecognized feature gate: %q", name)
152173
}
153174
if gate.LockToDefault && gate.Default != enabled {
154175
return fmt.Errorf("cannot set feature gate %q to %t, feature is locked to %t", name, enabled, gate.Default)
155176
}
156-
gate.Set(enabled)
177+
fv.setOne(gate.Name, enabled)
157178
}
158179

159180
return nil
160181
}
161182

162183
// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,...".
163184
func (fv *flagValue) String() string {
185+
//FIXME: should this list all gates or just ones that are set?
164186
pairs := []string{}
165187
for _, gate := range fv.gates.gateMap {
166188
pairs = append(pairs, fmt.Sprintf("%s=%t", gate.Name, Enabled(gate)))
@@ -176,14 +198,76 @@ func (fv *flagValue) Type() string {
176198
return "mapStringBool"
177199
}
178200

201+
func (gs *GateSet) EnableEnvControl(prefix string) {
202+
//FIXME: make sure all env vars with this prefix are known gates?
203+
gs.providers = append(gs.providers, &envProvider{prefix: prefix})
204+
}
205+
206+
// envProvider implements GateProvider.
207+
type envProvider struct {
208+
prefix string
209+
values map[string]bool
210+
}
211+
212+
// Implement GateProvider.
213+
func (ep *envProvider) Get(name string) (bool, bool) {
214+
if ep.values == nil {
215+
ep.values = map[string]bool{}
216+
}
217+
key := fmt.Sprintf("%s%s", ep.prefix, name)
218+
strVal, found := os.LookupEnv(key)
219+
if !found {
220+
return false, false
221+
}
222+
223+
boolVal, err := strconv.ParseBool(strVal)
224+
if err != nil {
225+
//FIXME: blech
226+
utilruntime.HandleError(fmt.Errorf("cannot set feature gate %q to %q: %v", key, strVal, err))
227+
}
228+
/* FIXME: check if gate is allowed to be overridden?
229+
if g.LockToDefault {
230+
if boolVal != g.Default {
231+
utilruntime.HandleError(fmt.Errorf("cannot set feature gate %q to %q: feature is locked to %t", feature, strVal, g.Default))
232+
}
233+
}
234+
*/
235+
return boolVal, true
236+
}
237+
238+
// FIXME: comments
239+
func (gs *GateSet) EnableExternalControl(provider GateProvider) {
240+
gs.providers = append(gs.providers, provider)
241+
}
242+
243+
// FIXME: comments
244+
func (gs *GateSet) Enabled(name string) (enabled bool, found bool) {
245+
foundAny := false
246+
for _, provider := range gs.providers {
247+
en, found := provider.Get(name)
248+
if !found {
249+
continue
250+
}
251+
foundAny = true
252+
if en {
253+
return true, true
254+
}
255+
}
256+
if foundAny {
257+
// We didn't early-return so we must have found a `false`
258+
return false, true
259+
}
260+
return false, false
261+
}
262+
179263
// Gate is a single feature gate definition.
180264
type Gate struct {
181-
Name string
182-
Default bool
183-
Release StabilityLevel
184-
LockToDefault bool
185-
enabled *bool
186-
readEnvVarsOnce sync.Once
265+
Name string
266+
Default bool
267+
Release StabilityLevel
268+
LockToDefault bool
269+
forcedValue *bool
270+
set *GateSet
187271
}
188272

189273
// StabilityLevel indicates which phase of the feature lifecycle a given gate is
@@ -198,30 +282,9 @@ const (
198282
)
199283

200284
// Set sets the value of this gate.
285+
// FIXME: comments regardless of other gate config sources
201286
func (g *Gate) Set(val bool) {
202-
g.enabled = &val
203-
}
204-
205-
// readEnvVar looks for an environment variable for this gate and (if found)
206-
207-
func (g *Gate) readEnvVar() {
208-
g.readEnvVarsOnce.Do(func() {
209-
feature := g.Name
210-
strVal, found := os.LookupEnv(fmt.Sprintf("KUBE_FEATURE_%s", feature))
211-
if !found {
212-
return
213-
}
214-
215-
if boolVal, err := strconv.ParseBool(strVal); err != nil {
216-
utilruntime.HandleError(fmt.Errorf("cannot set feature gate %q to %q: %v", feature, strVal, err))
217-
} else if g.LockToDefault {
218-
if boolVal != g.Default {
219-
utilruntime.HandleError(fmt.Errorf("cannot set feature gate %q to %q: feature is locked to %t", feature, strVal, g.Default))
220-
}
221-
} else {
222-
g.Set(boolVal)
223-
}
224-
})
287+
g.forcedValue = &val
225288
}
226289

227290
// MergeGateSets combines a list of GateSets into a new GateSet.
@@ -236,11 +299,21 @@ func MergeGateSets(gatesets ...*GateSet) *GateSet {
236299
// FIXME: This could be a function here or a method on Gate, but let's not do both?
237300
// Enabled returns whether the specified Gate is enabled.
238301
func Enabled(g *Gate) bool {
239-
g.readEnvVar()
240-
if g.enabled == nil {
302+
// If some code called Set(), we always respect that.
303+
if g.forcedValue != nil {
304+
return *(g.forcedValue)
305+
}
306+
// If the value is locked or this gate is not in a set, use the default.
307+
if g.LockToDefault || g.set == nil {
241308
return g.Default
242309
}
243-
return *g.enabled
310+
// Otherwise let the set's configured providers have an opinion.
311+
en, found := g.set.Enabled(g.Name)
312+
if found {
313+
return en
314+
}
315+
// Otherwise, use the default.
316+
return g.Default
244317
}
245318

246319
// testHelper is the locally-used subset of testing.T needed in SetForTesting.
@@ -254,10 +327,10 @@ type testHelper interface {
254327
func SetForTesting(g *Gate, t testHelper, enabled bool) func() {
255328
t.Helper()
256329

257-
prev := g.enabled
258-
g.enabled = &enabled
330+
prev := g.forcedValue
331+
g.forcedValue = &enabled
259332

260333
return func() {
261-
g.enabled = prev
334+
g.forcedValue = prev
262335
}
263336
}

0 commit comments

Comments
 (0)