Skip to content

Commit

Permalink
Merge pull request #271 from pjbgf/features
Browse files Browse the repository at this point in the history
runtime: Add Feature Gates
  • Loading branch information
Paulo Gomes authored May 11, 2022
2 parents d891f47 + 27d1fab commit 6ccf914
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 1 deletion.
85 changes: 85 additions & 0 deletions runtime/features/features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Copyright 2022 The Flux 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"

"github.com/go-logr/logr"
"github.com/spf13/pflag"
cliflag "k8s.io/component-base/cli/flag"
)

const (
flagFeatureGates = "feature-gates"
)

var featureGates map[string]bool
var loaded bool

// FeatureGates is a helper to manage feature switches.
//
// Controllers can set their supported features and then at runtime
// verify which ones are enabled/disabled.
//
// Callers have to call BindFlags, and then call SupportedFeatures to
// set the supported features and their default values.
type FeatureGates struct {
log *logr.Logger
cliFeatures map[string]bool
}

// WithLogger sets the logger to be used when loading supported features.
func (o *FeatureGates) WithLogger(l logr.Logger) *FeatureGates {
o.log = &l
return o
}

// SupportedFeatures sets the supported features and their default values.
func (o *FeatureGates) SupportedFeatures(features map[string]bool) error {
loaded = true
featureGates = features

for k, v := range o.cliFeatures {
if _, ok := featureGates[k]; ok {
featureGates[k] = v
} else {
return fmt.Errorf("feature-gate '%s' not supported", k)
}
if o.log != nil {
o.log.Info("loading feature gate", k, v)
}
}
return nil
}

// Enabled verifies whether the feature is enabled or not.
func Enabled(feature string) (bool, error) {
if !loaded {
return false, fmt.Errorf("supported features not set")
}
if enabled, ok := featureGates[feature]; ok {
return enabled, nil
}
return false, fmt.Errorf("feature-gate '%s' not supported", feature)
}

// BindFlags will parse the given pflag.FlagSet and load feature gates accordingly.
func (o *FeatureGates) BindFlags(fs *pflag.FlagSet) {
fs.Var(cliflag.NewMapStringBool(&o.cliFeatures), flagFeatureGates,
"A comma separated list of key=value pairs defining the state of experimental features.")
}
153 changes: 153 additions & 0 deletions runtime/features/features_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
Copyright 2022 The Flux 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/onsi/gomega"
"github.com/spf13/pflag"
)

func TestSupportedFeatures(t *testing.T) {
tests := []struct {
name string
supportedFeatures map[string]bool
commandLine []string
wantErr string
}{
{
name: "opt-in when default value is false",
commandLine: []string{"--feature-gates=invisible-messages=true"},
supportedFeatures: map[string]bool{"invisible-messages": false},
},
{
name: "opt-out when default value is true",
commandLine: []string{"--feature-gates=invisible-messages=false"},
supportedFeatures: map[string]bool{"invisible-messages": true},
},
{
name: "multiple feature gates",
commandLine: []string{"--feature-gates=invisible-messages=false,time-travel=true"},
supportedFeatures: map[string]bool{"invisible-messages": true, "time-travel": false},
},
{
name: "try set feature gate that is not supported",
commandLine: []string{"--feature-gates=time-travel=true"},
supportedFeatures: map[string]bool{},
wantErr: "feature-gate 'time-travel' not supported",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

fs := pflag.NewFlagSet("", pflag.ContinueOnError)

features := FeatureGates{}
features.BindFlags(fs)
fs.Parse(tt.commandLine)

err := features.SupportedFeatures(tt.supportedFeatures)
if tt.wantErr == "" {
g.Expect(err).ToNot(HaveOccurred())
} else {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).Should(ContainSubstring(tt.wantErr))
}
})
}
}

func TestEnabled(t *testing.T) {
tests := []struct {
name string
supportedFeatures map[string]bool
setSupportedFeatures bool
commandLine []string
featureGate string
enabled bool
wantErr string
}{
{
name: "opt-in when default value is false",
commandLine: []string{"--feature-gates=invisible-messages=true"},
featureGate: "invisible-messages",
supportedFeatures: map[string]bool{"invisible-messages": false},
setSupportedFeatures: true,
enabled: true,
},
{
name: "opt-out when default value is true",
commandLine: []string{"--feature-gates=invisible-messages=false"},
featureGate: "invisible-messages",
supportedFeatures: map[string]bool{"invisible-messages": true},
setSupportedFeatures: true,
enabled: false,
},
{
name: "multiple feature gates",
commandLine: []string{"--feature-gates=invisible-messages=false,time-travel=true"},
featureGate: "time-travel",
supportedFeatures: map[string]bool{"invisible-messages": true, "time-travel": false},
setSupportedFeatures: true,
enabled: true,
},
{
name: "try feature gate that is not supported",
featureGate: "time-travel",
supportedFeatures: map[string]bool{},
setSupportedFeatures: true,
wantErr: "feature-gate 'time-travel' not supported",
},
{
name: "supported features not set",
featureGate: "time-travel",
setSupportedFeatures: false,
wantErr: "supported features not set",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

loaded = false
fs := pflag.NewFlagSet("", pflag.ContinueOnError)

features := FeatureGates{}
features.BindFlags(fs)
fs.Parse(tt.commandLine)

if tt.setSupportedFeatures {
err := features.SupportedFeatures(tt.supportedFeatures)
g.Expect(err).ToNot(HaveOccurred())
}

enabled, err := Enabled(tt.featureGate)
if tt.wantErr == "" {
g.Expect(err).ToNot(HaveOccurred())
} else {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).Should(ContainSubstring(tt.wantErr))
}

g.Expect(enabled).To(Equal(tt.enabled))
})
}
}
4 changes: 3 additions & 1 deletion runtime/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
k8s.io/api v0.23.5
k8s.io/apimachinery v0.23.5
k8s.io/client-go v0.23.5
k8s.io/component-base v0.23.5
k8s.io/klog/v2 v2.50.0
sigs.k8s.io/controller-runtime v0.11.2
)
Expand All @@ -41,6 +42,7 @@ require (
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand All @@ -49,6 +51,7 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/cobra v1.4.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 // indirect
Expand All @@ -65,7 +68,6 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiextensions-apiserver v0.23.5 // indirect
k8s.io/component-base v0.23.5 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
Expand Down
5 changes: 5 additions & 0 deletions runtime/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -285,6 +286,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
Expand Down Expand Up @@ -416,6 +418,7 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
Expand All @@ -436,6 +439,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
Expand Down

0 comments on commit 6ccf914

Please sign in to comment.