diff --git a/conf/defaults.ini b/conf/defaults.ini index 8e0a113a0ec..080d4e62fe0 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -336,7 +336,7 @@ rudderstack_data_plane_url = rudderstack_sdk_url = # Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1 -rudderstack_v3_sdk_url = +rudderstack_v3_sdk_url = # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config rudderstack_config_url = @@ -2079,8 +2079,14 @@ enable = # To enable features by default, set `Expression: "true"` in: # https://github.com/grafana/grafana/blob/main/pkg/services/featuremgmt/registry.go +# The feature_toggles section supports feature flags of a number of types, +# including boolean, string, integer, float, and structured values, following the OpenFeature specification. +# # feature1 = true # feature2 = false +# feature3 = "foobar" +# feature4 = 1.5 +# feature5 = { "foo": "bar" } [feature_toggles.openfeature] # This is EXPERIMENTAL. Please, do not use this section diff --git a/conf/sample.ini b/conf/sample.ini index 5a579d0e74e..b4bc6027abf 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -323,7 +323,7 @@ ;rudderstack_sdk_url = # Rudderstack v3 SDK, optional, defaults to false. If set, Rudderstack v3 SDK will be used instead of v1 -;rudderstack_v3_sdk_url = +;rudderstack_v3_sdk_url = # Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config ;rudderstack_config_url = @@ -1913,7 +1913,7 @@ default_datasource_uid = # client_queue_max_size is the maximum size in bytes of the client queue # for Live connections. Defaults to 4MB. -;client_queue_max_size = +;client_queue_max_size = #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] @@ -1996,9 +1996,14 @@ default_datasource_uid = ;enable = feature1,feature2 +# The feature_toggles section supports feature flags of a number of types, +# including boolean, string, integer, float, and structured values, following the OpenFeature specification. + ;feature1 = true ;feature2 = false - +;feature3 = "foobar" +;feature4 = 1.5 +;feature5 = { "foo": "bar" } [date_formats] # For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/ diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 67c361b2bdc..d9125991c12 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2836,9 +2836,11 @@ For more information about Grafana Enterprise, refer to [Grafana Enterprise](../ Keys of features to enable, separated by space. -#### `FEATURE_TOGGLE_NAME = false` +#### `FEATURE_NAME = ` -Some feature toggles for stable features are on by default. Use this setting to disable an on-by-default feature toggle with the name FEATURE_TOGGLE_NAME, for example, `exploreMixedDatasource = false`. +Use a key-value pair to set feature flag values explicitly, overriding any default values. A few different types are supported, following the OpenFeature specification. See the defaults.ini file for more details. + +For example, to disable an on-by-default feature toggle named `exploreMixedDatasource`, specify `exploreMixedDatasource = false`.
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8bc45524b37..f1b1ce4154d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -622,10 +622,6 @@ export interface FeatureToggles { */ exploreLogsAggregatedMetrics?: boolean; /** - * Used in Logs Drilldown to limit the time range - */ - exploreLogsLimitedTimeRange?: boolean; - /** * Enables the gRPC client to authenticate with the App Platform by using ID & access tokens */ appPlatformGrpcClientAuth?: boolean; diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index d59dff63c37..72f9d5ffc5b 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -133,7 +133,11 @@ type FeatureFlag struct { Stage FeatureFlagStage `json:"stage,omitempty"` Owner codeowner `json:"-"` // Owner person or team that owns this feature flag - // CEL-GO expression. Using the value "true" will mean this is on by default + // Expression defined by the feature_toggles configuration. + // Supports multiple types including boolean, string, integer, float, + // and structured values following the OpenFeature specification. + // Using the value "true" means the feature flag is enabled by default, + // Using the value "1.0" means the default value of the feature flag is 1.0 Expression string `json:"expression,omitempty"` // Special behavior properties diff --git a/pkg/services/featuremgmt/openfeature.go b/pkg/services/featuremgmt/openfeature.go index cd3b77322fb..22017b8de03 100644 --- a/pkg/services/featuremgmt/openfeature.go +++ b/pkg/services/featuremgmt/openfeature.go @@ -8,6 +8,7 @@ import ( clientauthmiddleware "github.com/grafana/grafana/pkg/clientauth/middleware" "github.com/grafana/grafana/pkg/setting" + "github.com/open-feature/go-sdk/openfeature/memprovider" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/open-feature/go-sdk/openfeature" @@ -26,7 +27,7 @@ type OpenFeatureConfig struct { // HTTPClient is a pre-configured HTTP client (optional, used by features-service + OFREP providers) HTTPClient *http.Client // StaticFlags are the feature flags to use with static provider - StaticFlags map[string]bool + StaticFlags map[string]memprovider.InMemoryFlag // TargetingKey is used for evaluation context TargetingKey string // ContextAttrs are additional attributes for evaluation context @@ -100,7 +101,7 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error { func createProvider( providerType string, u *url.URL, - staticFlags map[string]bool, + staticFlags map[string]memprovider.InMemoryFlag, httpClient *http.Client, ) (openfeature.FeatureProvider, error) { if providerType == setting.FeaturesServiceProviderType || providerType == setting.OFREPProviderType { @@ -117,7 +118,7 @@ func createProvider( } } - return newStaticProvider(staticFlags) + return newStaticProvider(staticFlags, standardFeatureFlags) } func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3e108d1ed0c..c9f2ca03184 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1031,13 +1031,6 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, - { - Name: "exploreLogsLimitedTimeRange", - Description: "Used in Logs Drilldown to limit the time range", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaObservabilityLogsSquad, - }, { Name: "appPlatformGrpcClientAuth", Description: "Enables the gRPC client to authenticate with the App Platform by using ID & access tokens", diff --git a/pkg/services/featuremgmt/service.go b/pkg/services/featuremgmt/service.go index 2769a75d788..2c97666d9e9 100644 --- a/pkg/services/featuremgmt/service.go +++ b/pkg/services/featuremgmt/service.go @@ -47,7 +47,8 @@ func ProvideManagerService(cfg *setting.Cfg) (*FeatureManager, error) { } mgmt.warnings[key] = "unknown flag in config" } - mgmt.startup[key] = val + + mgmt.startup[key] = val.Variants[val.DefaultVariant] == true } // update the values diff --git a/pkg/services/featuremgmt/static_evaluator.go b/pkg/services/featuremgmt/static_evaluator.go index c3d46837d28..fdeef7a5858 100644 --- a/pkg/services/featuremgmt/static_evaluator.go +++ b/pkg/services/featuremgmt/static_evaluator.go @@ -29,7 +29,7 @@ func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) { return nil, fmt.Errorf("failed to read feature flags from config: %w", err) } - staticProvider, err := newStaticProvider(staticFlags) + staticProvider, err := newStaticProvider(staticFlags, standardFeatureFlags) if err != nil { return nil, fmt.Errorf("failed to create static provider: %w", err) } diff --git a/pkg/services/featuremgmt/static_provider.go b/pkg/services/featuremgmt/static_provider.go index f384bd00de1..f6fe14d7de9 100644 --- a/pkg/services/featuremgmt/static_provider.go +++ b/pkg/services/featuremgmt/static_provider.go @@ -1,8 +1,13 @@ package featuremgmt import ( + "fmt" + "maps" + "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/memprovider" + + "github.com/grafana/grafana/pkg/setting" ) // inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that @@ -28,37 +33,21 @@ func (p *inMemoryBulkProvider) ListFlags() ([]string, error) { return keys, nil } -func newStaticProvider(confFlags map[string]bool) (openfeature.FeatureProvider, error) { - flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags)) +func newStaticProvider(confFlags map[string]memprovider.InMemoryFlag, standardFlags []FeatureFlag) (openfeature.FeatureProvider, error) { + flags := make(map[string]memprovider.InMemoryFlag, len(standardFlags)) + + // Parse and add standard flags + for _, flag := range standardFlags { + inMemFlag, err := setting.ParseFlag(flag.Name, flag.Expression) + if err != nil { + return nil, fmt.Errorf("failed to parse flag %s: %w", flag.Name, err) + } + + flags[flag.Name] = inMemFlag + } // Add flags from config.ini file - for name, value := range confFlags { - flags[name] = createInMemoryFlag(name, value) - } - - // Add standard flags - for _, flag := range standardFeatureFlags { - if _, exists := flags[flag.Name]; !exists { - enabled := flag.Expression == "true" - flags[flag.Name] = createInMemoryFlag(flag.Name, enabled) - } - } + maps.Copy(flags, confFlags) return newInMemoryBulkProvider(flags), nil } - -func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag { - variant := "disabled" - if enabled { - variant = "enabled" - } - - return memprovider.InMemoryFlag{ - Key: name, - DefaultVariant: variant, - Variants: map[string]interface{}{ - "enabled": true, - "disabled": false, - }, - } -} diff --git a/pkg/services/featuremgmt/static_provider_test.go b/pkg/services/featuremgmt/static_provider_test.go index 29610baa8c4..c3245f7dd97 100644 --- a/pkg/services/featuremgmt/static_provider_test.go +++ b/pkg/services/featuremgmt/static_provider_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/setting" + "github.com/open-feature/go-sdk/openfeature/memprovider" "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" @@ -93,3 +94,144 @@ ABCD = true enabledFeatureManager := mgr.GetEnabled(ctx) assert.Equal(t, openFeatureEnabledFlags, enabledFeatureManager) } + +func Test_StaticProvider_TypedFlags(t *testing.T) { + tests := []struct { + flags FeatureFlag + defaultValue any + expectedValue any + }{ + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "true", + }, + defaultValue: false, + expectedValue: true, + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "1.0", + }, + defaultValue: 0.0, + expectedValue: 1.0, + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "blue", + }, + defaultValue: "red", + expectedValue: "blue", + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: "1", + }, + defaultValue: int64(0), + expectedValue: int64(1), + }, + { + flags: FeatureFlag{ + Name: "Flag", + Expression: `{ "foo": "bar" }`, + }, + expectedValue: map[string]any{"foo": "bar"}, + }, + } + + for _, tt := range tests { + provider, err := newStaticProvider(nil, []FeatureFlag{tt.flags}) + assert.NoError(t, err) + + var result any + switch tt.expectedValue.(type) { + case bool: + result = provider.BooleanEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(bool), openfeature.FlattenedContext{}).Value + case float64: + result = provider.FloatEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(float64), openfeature.FlattenedContext{}).Value + case string: + result = provider.StringEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(string), openfeature.FlattenedContext{}).Value + case int64: + result = provider.IntEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(int64), openfeature.FlattenedContext{}).Value + case map[string]any: + result = provider.ObjectEvaluation(t.Context(), tt.flags.Name, tt.defaultValue, openfeature.FlattenedContext{}).Value + } + + assert.Equal(t, tt.expectedValue, result) + } +} +func Test_StaticProvider_ConfigOverride(t *testing.T) { + tests := []struct { + name string + originalValue string + configValue any + }{ + { + name: "bool", + originalValue: "false", + configValue: true, + }, + { + name: "int", + originalValue: "0", + configValue: int64(1), + }, + { + name: "float", + originalValue: "0.0", + configValue: 1.0, + }, + { + name: "string", + originalValue: "foo", + configValue: "bar", + }, + { + name: "structure", + originalValue: "{}", + configValue: make(map[string]any), + }, + } + + for _, tt := range tests { + configFlags, standardFlags := makeFlags(tt) + provider, err := newStaticProvider(configFlags, standardFlags) + assert.NoError(t, err) + + var result any + switch tt.configValue.(type) { + case bool: + result = provider.BooleanEvaluation(t.Context(), tt.name, false, openfeature.FlattenedContext{}).Value + case float64: + result = provider.FloatEvaluation(t.Context(), tt.name, 0.0, openfeature.FlattenedContext{}).Value + case string: + result = provider.StringEvaluation(t.Context(), tt.name, "foo", openfeature.FlattenedContext{}).Value + case int64: + result = provider.IntEvaluation(t.Context(), tt.name, 1, openfeature.FlattenedContext{}).Value + case map[string]any: + result = provider.ObjectEvaluation(t.Context(), tt.name, make(map[string]any), openfeature.FlattenedContext{}).Value + } + + assert.Equal(t, tt.configValue, result) + } +} + +func makeFlags(tt struct { + name string + originalValue string + configValue any +}) (map[string]memprovider.InMemoryFlag, []FeatureFlag) { + orig := FeatureFlag{ + Name: tt.name, + Expression: tt.originalValue, + } + + config := map[string]memprovider.InMemoryFlag{ + tt.name: setting.NewInMemoryFlag(tt.name, tt.configValue), + } + + return config, []FeatureFlag{orig} +} diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 20ff391364e..7557323e43b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -142,7 +142,6 @@ vizActionsAuth,preview,@grafana/dataviz-squad,false,false,true alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true -exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 50383070847..49cdbd86374 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1382,7 +1382,8 @@ "metadata": { "name": "exploreLogsLimitedTimeRange", "resourceVersion": "1764664939750", - "creationTimestamp": "2024-08-29T13:55:59Z" + "creationTimestamp": "2024-08-29T13:55:59Z", + "deletionTimestamp": "2026-01-12T22:18:14Z" }, "spec": { "description": "Used in Logs Drilldown to limit the time range", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 57e308de4ec..dbfa4af0c1c 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -190,9 +190,6 @@ func verifyFlagsConfiguration(t *testing.T) { if flag.Stage == FeatureStageGeneralAvailability && flag.Expression == "" { t.Errorf("GA features must be explicitly enabled or disabled, please add the `Expression` property for %s", flag.Name) } - if flag.Expression != "" && flag.Expression != "true" && flag.Expression != "false" { - t.Errorf("the `Expression` property for %s is incorrect. valid values are: `true`, `false` or empty string for default", flag.Name) - } // Check camel case names if flag.Name != strcase.ToLowerCamel(flag.Name) && !legacyNames[flag.Name] { invalidNames = append(invalidNames, flag.Name) diff --git a/pkg/services/updatemanager/plugins_test.go b/pkg/services/updatemanager/plugins_test.go index 75834c44c7d..93b3d54b321 100644 --- a/pkg/services/updatemanager/plugins_test.go +++ b/pkg/services/updatemanager/plugins_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" @@ -378,8 +379,10 @@ func setupOpenFeatureProvider(t *testing.T, flagValue bool) { err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{ ProviderType: setting.StaticProviderType, - StaticFlags: map[string]bool{ - featuremgmt.FlagPluginsAutoUpdate: flagValue, + StaticFlags: map[string]memprovider.InMemoryFlag{ + featuremgmt.FlagPluginsAutoUpdate: { + Key: featuremgmt.FlagPluginsAutoUpdate, Variants: map[string]any{"": flagValue}, + }, }, }) require.NoError(t, err) diff --git a/pkg/setting/setting_feature_toggles.go b/pkg/setting/setting_feature_toggles.go index e09b45e9edb..38bfd0269e1 100644 --- a/pkg/setting/setting_feature_toggles.go +++ b/pkg/setting/setting_feature_toggles.go @@ -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) + } +} diff --git a/pkg/setting/setting_feature_toggles_test.go b/pkg/setting/setting_feature_toggles_test.go index b0c3730bcad..040a9ef7427 100644 --- a/pkg/setting/setting_feature_toggles_test.go +++ b/pkg/setting/setting_feature_toggles_test.go @@ -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) } } } diff --git a/pkg/storage/unified/testing/storage_backend_sql_compatibility.go b/pkg/storage/unified/testing/storage_backend_sql_compatibility.go index 9066a39221c..5e1423fa1ec 100644 --- a/pkg/storage/unified/testing/storage_backend_sql_compatibility.go +++ b/pkg/storage/unified/testing/storage_backend_sql_compatibility.go @@ -81,6 +81,12 @@ func RunSQLStorageBackendCompatibilityTest(t *testing.T, newSqlBackend, newKvBac kvbackend, db := newKvBackend(t.Context()) sqlbackend, _ := newSqlBackend(t.Context()) + + // Skip on SQLite due to concurrency limitations + if db.DriverName() == "sqlite3" { + t.Skip("Skipping concurrent operations stress test on SQLite") + } + tc.fn(t, sqlbackend, kvbackend, opts.NSPrefix, db) }) } @@ -686,11 +692,6 @@ func runTestCrossBackendConsistency(t *testing.T, sqlBackend, kvBackend resource // runTestConcurrentOperationsStress tests heavy concurrent operations between SQL and KV backends func runTestConcurrentOperationsStress(t *testing.T, sqlBackend, kvBackend resource.StorageBackend, nsPrefix string, db sqldb.DB) { - // Skip on SQLite due to concurrency limitations - if db.DriverName() == "sqlite3" { - t.Skip("Skipping concurrent operations stress test on SQLite") - } - ctx := testutil.NewDefaultTestContext(t) // Create storage servers from both backends diff --git a/pkg/tests/apis/features/features_test.go b/pkg/tests/apis/features/features_test.go index ab7bfea64d4..0a8c9564492 100644 --- a/pkg/tests/apis/features/features_test.go +++ b/pkg/tests/apis/features/features_test.go @@ -44,6 +44,6 @@ func TestIntegrationFeatures(t *testing.T) { "value": true, "key":"`+flag+`", "reason":"static provider evaluation result", - "variant":"enabled"}`, string(rsp.Body)) + "variant":"default"}`, string(rsp.Body)) }) } diff --git a/pkg/util/testutil/context_test.go b/pkg/util/testutil/context_test.go index 4d7ecf670f6..ca5c5abee4f 100644 --- a/pkg/util/testutil/context_test.go +++ b/pkg/util/testutil/context_test.go @@ -15,7 +15,10 @@ import ( func TestMain(m *testing.M) { // make sure we don't leak goroutines after tests in this package have // finished, which means we haven't leaked contexts either - goleak.VerifyTestMain(m) + // (Except for goroutines running specific functions. If possible we should fix this.) + goleak.VerifyTestMain(m, + goleak.IgnoreTopFunction("github.com/open-feature/go-sdk/openfeature.(*eventExecutor).startEventListener.func1.1"), + ) } func TestTestContextFunc(t *testing.T) {