diff --git a/conf/defaults.ini b/conf/defaults.ini index 963b9afd2e7..91b39b47808 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1076,9 +1076,17 @@ grpc_port = license_path = [feature_toggles] -# enable features, separated by spaces +# there are currently two ways to enable feature toggles in the `grafana.ini`. +# you can either pass an array of feature you want to enable to the `enable` field or +# configure each toggle by setting the name of the toggle to true/false. Toggles set to true/false +# will take presidence over toggles in the `enable` list. + +# enable = feature1,feature2 enable = +# feature1 = true +# feature2 = false + [date_formats] # For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/ diff --git a/conf/sample.ini b/conf/sample.ini index e7634b55fca..0a98f786626 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1055,8 +1055,15 @@ ;license_path = [feature_toggles] -# enable features, separated by spaces -;enable = +# there are currently two ways to enable feature toggles in the `grafana.ini`. +# you can either pass an array of feature you want to enable to the `enable` field or +# configure each toggle by setting the name of the toggle to true/false. Toggles set to true/false +# will take presidence over toggles in the `enable` list. + +;enable = feature1,feature2 + +;feature1 = true +;feature2 = false [date_formats] # For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/ diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 29cd0154868..b8256c7b25a 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1407,17 +1407,6 @@ func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error { return nil } -func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error { - // Read and populate feature toggles list - featureTogglesSection := iniFile.Section("feature_toggles") - cfg.FeatureToggles = make(map[string]bool) - featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "") - for _, feature := range util.SplitString(featuresTogglesStr) { - cfg.FeatureToggles[feature] = true - } - return nil -} - func readAlertingSettings(iniFile *ini.File) error { alerting := iniFile.Section("alerting") enabled, err := alerting.Key("enabled").Bool() diff --git a/pkg/setting/setting_feature_toggles.go b/pkg/setting/setting_feature_toggles.go new file mode 100644 index 00000000000..4c103b31030 --- /dev/null +++ b/pkg/setting/setting_feature_toggles.go @@ -0,0 +1,74 @@ +package setting + +import ( + "strconv" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/util" + "gopkg.in/ini.v1" +) + +var ( + featureToggleInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "feature_toggles_info", + Help: "info metric that exposes what feature toggles are enabled or not", + Namespace: "grafana", + }, []string{"name"}) + + defaultFeatureToggles = map[string]bool{ + "recordedQueries": false, + "accesscontrol": false, + "service-accounts": false, + "httpclientprovider_azure_auth": false, + } +) + +func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error { + toggles, err := overrideDefaultWithConfiguration(iniFile, defaultFeatureToggles) + if err != nil { + return err + } + + cfg.FeatureToggles = toggles + + return nil +} + +func overrideDefaultWithConfiguration(iniFile *ini.File, featureToggles map[string]bool) (map[string]bool, error) { + // Read and populate feature toggles list + featureTogglesSection := iniFile.Section("feature_toggles") + + // parse the comma separated list in `enable`. + featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "") + for _, feature := range util.SplitString(featuresTogglesStr) { + featureToggles[feature] = true + } + + // read all other settings under [feature_toggles]. If a toggle is + // present in both the value in `enable` is overridden. + for _, v := range featureTogglesSection.Keys() { + if v.Name() == "enable" { + continue + } + + b, err := strconv.ParseBool(v.Value()) + if err != nil { + return featureToggles, err + } + + featureToggles[v.Name()] = b + } + + // track if feature toggles are enabled or not using an info metric + for k, v := range featureToggles { + if v { + featureToggleInfo.WithLabelValues(k).Set(1) + } else { + featureToggleInfo.WithLabelValues(k).Set(0) + } + } + + return featureToggles, nil +} diff --git a/pkg/setting/setting_feature_toggles_test.go b/pkg/setting/setting_feature_toggles_test.go new file mode 100644 index 00000000000..92ec0f03095 --- /dev/null +++ b/pkg/setting/setting_feature_toggles_test.go @@ -0,0 +1,98 @@ +package setting + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" +) + +func TestFeatureToggles(t *testing.T) { + testCases := []struct { + name string + conf map[string]string + err error + expectedToggles map[string]bool + defaultToggles map[string]bool + }{ + { + name: "can parse feature toggles passed in the `enable` array", + conf: map[string]string{ + "enable": "feature1,feature2", + }, + expectedToggles: map[string]bool{ + "feature1": true, + "feature2": true, + }, + }, + { + name: "can parse feature toggles listed under [feature_toggles]", + conf: map[string]string{ + "enable": "feature1,feature2", + "feature3": "true", + }, + expectedToggles: map[string]bool{ + "feature1": true, + "feature2": true, + "feature3": true, + }, + }, + { + name: "toggles under [feature_toggles] overrides those in the array", + conf: map[string]string{ + "enable": "feature1,feature2", + "feature2": "false", + }, + expectedToggles: map[string]bool{ + "feature1": true, + "feature2": false, + }, + }, + { + name: "invalid boolean value should return syntax error", + conf: map[string]string{ + "enable": "feature1,feature2", + "feature2": "invalid", + }, + expectedToggles: map[string]bool{}, + err: strconv.ErrSyntax, + }, + { + name: "should override default feature toggles", + defaultToggles: map[string]bool{ + "feature1": true, + }, + conf: map[string]string{ + "feature1": "false", + }, + expectedToggles: map[string]bool{ + "feature1": false, + }, + }, + } + + for _, tc := range testCases { + f := ini.Empty() + + toggles, _ := f.NewSection("feature_toggles") + for k, v := range tc.conf { + _, err := toggles.NewKey(k, v) + require.ErrorIs(t, err, nil) + } + + dt := map[string]bool{} + if len(tc.defaultToggles) > 0 { + dt = tc.defaultToggles + } + + featureToggles, err := overrideDefaultWithConfiguration(f, dt) + require.ErrorIs(t, err, tc.err) + + if err == nil { + for k, v := range featureToggles { + require.Equal(t, tc.expectedToggles[k], v, tc.name) + } + } + } +}