From 27d1faba1072cc503838200ca0e5195e87154376 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Wed, 11 May 2022 10:00:53 +0100 Subject: [PATCH] runtime: Add Features Gates Signed-off-by: Paulo Gomes --- runtime/features/features.go | 85 +++++++++++++++++ runtime/features/features_test.go | 153 ++++++++++++++++++++++++++++++ runtime/go.mod | 4 +- runtime/go.sum | 5 + 4 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 runtime/features/features.go create mode 100644 runtime/features/features_test.go diff --git a/runtime/features/features.go b/runtime/features/features.go new file mode 100644 index 00000000..5967296a --- /dev/null +++ b/runtime/features/features.go @@ -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.") +} diff --git a/runtime/features/features_test.go b/runtime/features/features_test.go new file mode 100644 index 00000000..d38b7ba3 --- /dev/null +++ b/runtime/features/features_test.go @@ -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)) + }) + } +} diff --git a/runtime/go.mod b/runtime/go.mod index a0737a81..e171e050 100644 --- a/runtime/go.mod +++ b/runtime/go.mod @@ -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 ) @@ -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 @@ -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 @@ -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 diff --git a/runtime/go.sum b/runtime/go.sum index 39bfe5dc..dbd7cef0 100644 --- a/runtime/go.sum +++ b/runtime/go.sum @@ -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= @@ -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= @@ -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= @@ -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=