diff --git a/pkg/setting/setting_feature_toggles.go b/pkg/setting/setting_feature_toggles.go index e09b45e9edb..9290f0d6127 100644 --- a/pkg/setting/setting_feature_toggles.go +++ b/pkg/setting/setting_feature_toggles.go @@ -1,6 +1,8 @@ package setting import ( + "encoding/json" + "fmt" "strconv" "gopkg.in/ini.v1" @@ -8,6 +10,20 @@ import ( "github.com/grafana/grafana/pkg/util" ) +type FeatureToggleType string + +const Structure FeatureToggleType = "structure" +const Integer FeatureToggleType = "integer" +const Float FeatureToggleType = "float" +const Boolean FeatureToggleType = "boolean" +const String FeatureToggleType = "string" + +type FeatureToggle struct { + Type FeatureToggleType `json:"type"` + Name string `json:"name"` + Value any `json:"value"` +} + // Deprecated: should use `featuremgmt.FeatureToggles` func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error { section := iniFile.Section("feature_toggles") @@ -15,18 +31,31 @@ func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error { if err != nil { return err } + // TODO IsFeatureToggleEnabled has been deprecated for 2 years now, we should remove this function completely // nolint:staticcheck - cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] } + cfg.IsFeatureToggleEnabled = func(key string) bool { + + toggle, ok := toggles[key] + if !ok { + return false + } + + return toggle.Type == Boolean && toggle.Value == true + } return nil } -func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) { - featureToggles := make(map[string]bool, 10) +func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]FeatureToggle, error) { + featureToggles := make(map[string]FeatureToggle, 10) // parse the comma separated list in `enable`. featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "") for _, feature := range util.SplitString(featuresTogglesStr) { - featureToggles[feature] = true + featureToggles[feature] = FeatureToggle{ + Type: Boolean, + Name: feature, + Value: true, + } } // read all other settings under [feature_toggles]. If a toggle is @@ -36,12 +65,35 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str continue } - b, err := strconv.ParseBool(v.Value()) + b, err := ParseFlag(v.Name(), v.Value()) if err != nil { return featureToggles, err } + flag, exists := featureToggles[v.Name()] + if exists && flag.Type != b.Type { + return nil, fmt.Errorf("type mismatch during flag declaration '%s': %s, %s", v.Name(), flag.Type, b.Type) + } + featureToggles[v.Name()] = b } return featureToggles, nil } + +func ParseFlag(name, value string) (FeatureToggle, error) { + var structure any + + if boolean, err := strconv.ParseBool(value); err == nil { + return FeatureToggle{Type: Boolean, Name: name, Value: boolean}, nil + } + if integer, err := strconv.Atoi(value); err == nil { + return FeatureToggle{Type: Integer, Name: name, Value: integer}, nil + } + if float, err := strconv.ParseFloat(value, 64); err == nil { + return FeatureToggle{Type: Float, Name: name, Value: float}, nil + } + if err := json.Unmarshal([]byte(value), &structure); err == nil { + return FeatureToggle{Type: Structure, Name: name, Value: structure}, nil + } + return FeatureToggle{Type: String, Name: name, Value: value}, nil +} diff --git a/pkg/setting/setting_feature_toggles_test.go b/pkg/setting/setting_feature_toggles_test.go index b0c3730bcad..81e77dc4ea7 100644 --- a/pkg/setting/setting_feature_toggles_test.go +++ b/pkg/setting/setting_feature_toggles_test.go @@ -1,7 +1,7 @@ package setting import ( - "strconv" + "errors" "testing" "github.com/stretchr/testify/require" @@ -13,16 +13,16 @@ func TestFeatureToggles(t *testing.T) { name string conf map[string]string err error - expectedToggles map[string]bool + expectedToggles map[string]FeatureToggle }{ { 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, + expectedToggles: map[string]FeatureToggle{ + "feature1": {Name: "feature1", Type: Boolean, Value: true}, + "feature2": {Name: "feature2", Type: Boolean, Value: true}, }, }, { @@ -31,10 +31,10 @@ func TestFeatureToggles(t *testing.T) { "enable": "feature1,feature2", "feature3": "true", }, - expectedToggles: map[string]bool{ - "feature1": true, - "feature2": true, - "feature3": true, + expectedToggles: map[string]FeatureToggle{ + "feature1": {Name: "feature1", Type: Boolean, Value: true}, + "feature2": {Name: "feature2", Type: Boolean, Value: true}, + "feature3": {Name: "feature3", Type: Boolean, Value: true}, }, }, { @@ -43,9 +43,9 @@ func TestFeatureToggles(t *testing.T) { "enable": "feature1,feature2", "feature2": "false", }, - expectedToggles: map[string]bool{ - "feature1": true, - "feature2": false, + expectedToggles: map[string]FeatureToggle{ + "feature1": {Name: "feature1", Type: Boolean, Value: true}, + "feature2": {Name: "feature2", Type: Boolean, Value: false}, }, }, { @@ -54,8 +54,8 @@ func TestFeatureToggles(t *testing.T) { "enable": "feature1,feature2", "feature2": "invalid", }, - expectedToggles: map[string]bool{}, - err: strconv.ErrSyntax, + expectedToggles: map[string]FeatureToggle{}, + err: errors.New("type mismatch during flag declaration 'feature2': boolean, string"), }, } @@ -69,11 +69,14 @@ func TestFeatureToggles(t *testing.T) { } featureToggles, err := ReadFeatureTogglesFromInitFile(toggles) - require.ErrorIs(t, err, tc.err) + if tc.err != nil { + require.EqualError(t, err, tc.err.Error()) + } if err == nil { for k, v := range featureToggles { - require.Equal(t, tc.expectedToggles[k], v, tc.name) + toggle, _ := tc.expectedToggles[k] + require.Equal(t, toggle, v, tc.name) } } }