Add non-boolean feature flags support to the StaticProvider (#115085)
* initial commit * add support of integerts * finialise the static provider * minor refactoring * the rest * revert: the rest * add new thiongs * more tests added * add ff parsing tests to check if types are handled correctly * update tests according to recent changes * address golint issues * Update pkg/setting/setting_feature_toggles.go Co-authored-by: Dave Henderson <dave.henderson@grafana.com> * fix rebase issues * addressing review comments * add test cases for enterprise * handle enterprise cases * minor refactoring to make api a bit easier to debug * make test names a bit more precise * fix linter * add openfeature sdk to goleak ignore in testutil * Remove only boolean check in ff gen tests * add non-boolean types top the doc in default.ini and doc string in FeatureFlag type * apply remarks, add docs to sample.ini * reflect changes in feature flags in the public grafana configuration doc * fix doc formatting * apply suggestions to the doc file --------- Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
This commit is contained in:
co-authored by
Dave Henderson
parent
8c8efd2494
commit
ce9ab6a89a
@@ -1,13 +1,20 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
// DefaultVariantName a placeholder name for config-based Feature Flags
|
||||
const DefaultVariantName = "default"
|
||||
|
||||
// Deprecated: should use `featuremgmt.FeatureToggles`
|
||||
func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
||||
section := iniFile.Section("feature_toggles")
|
||||
@@ -15,18 +22,27 @@ 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
|
||||
}
|
||||
|
||||
value, ok := toggle.Variants[toggle.DefaultVariant].(bool)
|
||||
return value && ok
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
|
||||
featureToggles := make(map[string]bool, 10)
|
||||
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]memprovider.InMemoryFlag, error) {
|
||||
featureToggles := make(map[string]memprovider.InMemoryFlag, 10)
|
||||
|
||||
// parse the comma separated list in `enable`.
|
||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||
for _, feature := range util.SplitString(featuresTogglesStr) {
|
||||
featureToggles[feature] = true
|
||||
featureToggles[feature] = memprovider.InMemoryFlag{Key: feature, DefaultVariant: DefaultVariantName, Variants: map[string]any{DefaultVariantName: true}}
|
||||
}
|
||||
|
||||
// read all other settings under [feature_toggles]. If a toggle is
|
||||
@@ -36,7 +52,7 @@ 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
|
||||
}
|
||||
@@ -45,3 +61,57 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str
|
||||
}
|
||||
return featureToggles, nil
|
||||
}
|
||||
|
||||
func ParseFlag(name, value string) (memprovider.InMemoryFlag, error) {
|
||||
var structure map[string]any
|
||||
|
||||
if integer, err := strconv.Atoi(value); err == nil {
|
||||
return NewInMemoryFlag(name, integer), nil
|
||||
}
|
||||
if float, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return NewInMemoryFlag(name, float), nil
|
||||
}
|
||||
if err := json.Unmarshal([]byte(value), &structure); err == nil {
|
||||
return NewInMemoryFlag(name, structure), nil
|
||||
}
|
||||
if boolean, err := strconv.ParseBool(value); err == nil {
|
||||
return NewInMemoryFlag(name, boolean), nil
|
||||
}
|
||||
|
||||
return NewInMemoryFlag(name, value), nil
|
||||
}
|
||||
|
||||
func NewInMemoryFlag(name string, value any) memprovider.InMemoryFlag {
|
||||
return memprovider.InMemoryFlag{Key: name, DefaultVariant: DefaultVariantName, Variants: map[string]any{DefaultVariantName: value}}
|
||||
}
|
||||
|
||||
func AsStringMap(m map[string]memprovider.InMemoryFlag) map[string]string {
|
||||
var res = map[string]string{}
|
||||
for k, v := range m {
|
||||
res[k] = serializeFlagValue(v)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func serializeFlagValue(flag memprovider.InMemoryFlag) string {
|
||||
value := flag.Variants[flag.DefaultVariant]
|
||||
|
||||
switch castedValue := value.(type) {
|
||||
case bool:
|
||||
return strconv.FormatBool(castedValue)
|
||||
case int64:
|
||||
return strconv.FormatInt(castedValue, 10)
|
||||
case float64:
|
||||
// handle cases with a single or no zeros after the decimal point
|
||||
if math.Trunc(castedValue) == castedValue {
|
||||
return strconv.FormatFloat(castedValue, 'f', 1, 64)
|
||||
}
|
||||
|
||||
return strconv.FormatFloat(castedValue, 'g', -1, 64)
|
||||
case string:
|
||||
return castedValue
|
||||
default:
|
||||
val, _ := json.Marshal(value)
|
||||
return string(val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
@@ -12,17 +14,16 @@ func TestFeatureToggles(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf map[string]string
|
||||
err error
|
||||
expectedToggles map[string]bool
|
||||
expectedToggles map[string]memprovider.InMemoryFlag
|
||||
}{
|
||||
{
|
||||
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]memprovider.InMemoryFlag{
|
||||
"feature1": NewInMemoryFlag("feature1", true),
|
||||
"feature2": NewInMemoryFlag("feature2", true),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -31,10 +32,10 @@ func TestFeatureToggles(t *testing.T) {
|
||||
"enable": "feature1,feature2",
|
||||
"feature3": "true",
|
||||
},
|
||||
expectedToggles: map[string]bool{
|
||||
"feature1": true,
|
||||
"feature2": true,
|
||||
"feature3": true,
|
||||
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||
"feature1": NewInMemoryFlag("feature1", true),
|
||||
"feature2": NewInMemoryFlag("feature2", true),
|
||||
"feature3": NewInMemoryFlag("feature3", true),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -43,19 +44,26 @@ func TestFeatureToggles(t *testing.T) {
|
||||
"enable": "feature1,feature2",
|
||||
"feature2": "false",
|
||||
},
|
||||
expectedToggles: map[string]bool{
|
||||
"feature1": true,
|
||||
"feature2": false,
|
||||
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||
"feature1": NewInMemoryFlag("feature1", true),
|
||||
"feature2": NewInMemoryFlag("feature2", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid boolean value should return syntax error",
|
||||
name: "feature flags of different types are handled correctly",
|
||||
conf: map[string]string{
|
||||
"enable": "feature1,feature2",
|
||||
"feature2": "invalid",
|
||||
"feature1": "1", "feature2": "1.0",
|
||||
"feature3": `{"foo":"bar"}`, "feature4": "bar",
|
||||
"feature5": "t", "feature6": "T",
|
||||
},
|
||||
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||
"feature1": NewInMemoryFlag("feature1", 1),
|
||||
"feature2": NewInMemoryFlag("feature2", 1.0),
|
||||
"feature3": NewInMemoryFlag("feature3", map[string]any{"foo": "bar"}),
|
||||
"feature4": NewInMemoryFlag("feature4", "bar"),
|
||||
"feature5": NewInMemoryFlag("feature5", true),
|
||||
"feature6": NewInMemoryFlag("feature6", true),
|
||||
},
|
||||
expectedToggles: map[string]bool{},
|
||||
err: strconv.ErrSyntax,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -69,12 +77,35 @@ func TestFeatureToggles(t *testing.T) {
|
||||
}
|
||||
|
||||
featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
require.NoError(t, err)
|
||||
|
||||
if err == nil {
|
||||
for k, v := range featureToggles {
|
||||
require.Equal(t, tc.expectedToggles[k], v, tc.name)
|
||||
}
|
||||
for k, v := range featureToggles {
|
||||
toggle := tc.expectedToggles[k]
|
||||
require.Equal(t, toggle, v, tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagValueSerialization(t *testing.T) {
|
||||
testCases := []memprovider.InMemoryFlag{
|
||||
NewInMemoryFlag("int", 1),
|
||||
NewInMemoryFlag("1.0f", 1.0),
|
||||
NewInMemoryFlag("1.01f", 1.01),
|
||||
NewInMemoryFlag("1.10f", 1.10),
|
||||
NewInMemoryFlag("struct", map[string]any{"foo": "bar"}),
|
||||
NewInMemoryFlag("string", "bar"),
|
||||
NewInMemoryFlag("true", true),
|
||||
NewInMemoryFlag("false", false),
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
asStringMap := AsStringMap(map[string]memprovider.InMemoryFlag{tt.Key: tt})
|
||||
|
||||
deserialized, err := ParseFlag(tt.Key, asStringMap[tt.Key])
|
||||
assert.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(tt, deserialized); diff != "" {
|
||||
t.Errorf("(-want, +got) = %v", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user