Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15a9267326 | |||
| b47921d563 | |||
| 9fbd4d890c | |||
| 109a6ff707 | |||
| 15c8cfb762 | |||
| 8db4c70e7c | |||
| 3035a9f301 | |||
| 0616aae6f7 | |||
| e66240e087 | |||
| 5bfb689c58 | |||
| fac1dc03c0 | |||
| 55b83b42e9 | |||
| d8ca9bae7b | |||
| 9d7b3c3cb2 | |||
| 41d7213d7e | |||
| efad6c7be0 | |||
| e116254f32 |
@@ -33,12 +33,14 @@ require (
|
|||||||
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
||||||
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // @grafana/grafana-operator-experience-squad
|
||||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // @grafana/grafana-operator-experience-squad
|
||||||
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
||||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||||
@@ -343,7 +345,6 @@ require (
|
|||||||
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||||
@@ -358,7 +359,6 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
|
||||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||||
_ "github.com/Azure/go-autorest/autorest"
|
_ "github.com/Azure/go-autorest/autorest"
|
||||||
_ "github.com/Azure/go-autorest/autorest/adal"
|
_ "github.com/Azure/go-autorest/autorest/adal"
|
||||||
|
_ "github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||||
|
_ "github.com/aws/aws-sdk-go-v2/service/sts"
|
||||||
_ "github.com/beevik/etree"
|
_ "github.com/beevik/etree"
|
||||||
_ "github.com/blugelabs/bluge"
|
_ "github.com/blugelabs/bluge"
|
||||||
_ "github.com/blugelabs/bluge_segment_api"
|
_ "github.com/blugelabs/bluge_segment_api"
|
||||||
@@ -46,7 +49,6 @@ import (
|
|||||||
_ "sigs.k8s.io/randfill"
|
_ "sigs.k8s.io/randfill"
|
||||||
_ "xorm.io/builder"
|
_ "xorm.io/builder"
|
||||||
|
|
||||||
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
|
||||||
_ "github.com/grafana/authlib/authn"
|
_ "github.com/grafana/authlib/authn"
|
||||||
_ "github.com/grafana/authlib/authz"
|
_ "github.com/grafana/authlib/authz"
|
||||||
_ "github.com/grafana/authlib/cache"
|
_ "github.com/grafana/authlib/cache"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||||
"github.com/open-feature/go-sdk/openfeature"
|
"github.com/open-feature/go-sdk/openfeature"
|
||||||
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,7 +27,7 @@ type OpenFeatureConfig struct {
|
|||||||
// HTTPClient is a pre-configured HTTP client (optional, used by features-service + OFREP providers)
|
// HTTPClient is a pre-configured HTTP client (optional, used by features-service + OFREP providers)
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
// StaticFlags are the feature flags to use with static provider
|
// 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 is used for evaluation context
|
||||||
TargetingKey string
|
TargetingKey string
|
||||||
// ContextAttrs are additional attributes for evaluation context
|
// ContextAttrs are additional attributes for evaluation context
|
||||||
@@ -100,7 +101,7 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
|
|||||||
func createProvider(
|
func createProvider(
|
||||||
providerType string,
|
providerType string,
|
||||||
u *url.URL,
|
u *url.URL,
|
||||||
staticFlags map[string]bool,
|
staticFlags map[string]memprovider.InMemoryFlag,
|
||||||
httpClient *http.Client,
|
httpClient *http.Client,
|
||||||
) (openfeature.FeatureProvider, error) {
|
) (openfeature.FeatureProvider, error) {
|
||||||
if providerType == setting.FeaturesServiceProviderType || providerType == setting.OFREPProviderType {
|
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) {
|
func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) {
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ func ProvideManagerService(cfg *setting.Cfg) (*FeatureManager, error) {
|
|||||||
}
|
}
|
||||||
mgmt.warnings[key] = "unknown flag in config"
|
mgmt.warnings[key] = "unknown flag in config"
|
||||||
}
|
}
|
||||||
mgmt.startup[key] = val
|
|
||||||
|
mgmt.startup[key] = val.Variants[val.DefaultVariant] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the values
|
// update the values
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) {
|
|||||||
return nil, fmt.Errorf("failed to read feature flags from config: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create static provider: %w", err)
|
return nil, fmt.Errorf("failed to create static provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package featuremgmt
|
package featuremgmt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
|
||||||
"github.com/open-feature/go-sdk/openfeature"
|
"github.com/open-feature/go-sdk/openfeature"
|
||||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that
|
// inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that
|
||||||
@@ -28,37 +33,21 @@ func (p *inMemoryBulkProvider) ListFlags() ([]string, error) {
|
|||||||
return keys, nil
|
return keys, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStaticProvider(confFlags map[string]bool) (openfeature.FeatureProvider, error) {
|
func newStaticProvider(confFlags map[string]memprovider.InMemoryFlag, standardFlags []FeatureFlag) (openfeature.FeatureProvider, error) {
|
||||||
flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags))
|
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
|
// Add flags from config.ini file
|
||||||
for name, value := range confFlags {
|
maps.Copy(flags, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newInMemoryBulkProvider(flags), nil
|
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
"github.com/open-feature/go-sdk/openfeature"
|
"github.com/open-feature/go-sdk/openfeature"
|
||||||
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -93,3 +94,147 @@ ABCD = true
|
|||||||
enabledFeatureManager := mgr.GetEnabled(ctx)
|
enabledFeatureManager := mgr.GetEnabled(ctx)
|
||||||
assert.Equal(t, openFeatureEnabledFlags, enabledFeatureManager)
|
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: {
|
||||||
|
Key: tt.name,
|
||||||
|
Variants: map[string]any{"": tt.configValue},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, []FeatureFlag{orig}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/open-feature/go-sdk/openfeature"
|
"github.com/open-feature/go-sdk/openfeature"
|
||||||
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
@@ -378,8 +379,10 @@ func setupOpenFeatureProvider(t *testing.T, flagValue bool) {
|
|||||||
|
|
||||||
err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{
|
err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{
|
||||||
ProviderType: setting.StaticProviderType,
|
ProviderType: setting.StaticProviderType,
|
||||||
StaticFlags: map[string]bool{
|
StaticFlags: map[string]memprovider.InMemoryFlag{
|
||||||
featuremgmt.FlagPluginsAutoUpdate: flagValue,
|
featuremgmt.FlagPluginsAutoUpdate: {
|
||||||
|
Key: featuremgmt.FlagPluginsAutoUpdate, Variants: map[string]any{"": flagValue},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package setting
|
package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
|
|
||||||
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,18 +18,27 @@ func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// TODO IsFeatureToggleEnabled has been deprecated for 2 years now, we should remove this function completely
|
||||||
// nolint:staticcheck
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := toggle.Variants[toggle.DefaultVariant].(bool)
|
||||||
|
return ok && val
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
|
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]memprovider.InMemoryFlag, error) {
|
||||||
featureToggles := make(map[string]bool, 10)
|
featureToggles := make(map[string]memprovider.InMemoryFlag, 10)
|
||||||
|
|
||||||
// parse the comma separated list in `enable`.
|
// parse the comma separated list in `enable`.
|
||||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||||
for _, feature := range util.SplitString(featuresTogglesStr) {
|
for _, feature := range util.SplitString(featuresTogglesStr) {
|
||||||
featureToggles[feature] = true
|
featureToggles[feature] = memprovider.InMemoryFlag{Key: feature, Variants: map[string]any{"": true}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read all other settings under [feature_toggles]. If a toggle is
|
// read all other settings under [feature_toggles]. If a toggle is
|
||||||
@@ -36,7 +48,7 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := strconv.ParseBool(v.Value())
|
b, err := ParseFlag(v.Name(), v.Value())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return featureToggles, err
|
return featureToggles, err
|
||||||
}
|
}
|
||||||
@@ -45,3 +57,24 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str
|
|||||||
}
|
}
|
||||||
return featureToggles, nil
|
return featureToggles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseFlag(name, value string) (memprovider.InMemoryFlag, error) {
|
||||||
|
if integer, err := strconv.Atoi(value); err == nil {
|
||||||
|
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": integer}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if float, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": float}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var structure map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(value), &structure); err == nil {
|
||||||
|
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": structure}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if boolean, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": boolean}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": value}}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package setting
|
package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
@@ -12,17 +12,16 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
conf map[string]string
|
conf map[string]string
|
||||||
err error
|
expectedToggles map[string]memprovider.InMemoryFlag
|
||||||
expectedToggles map[string]bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "can parse feature toggles passed in the `enable` array",
|
name: "can parse feature toggles passed in the `enable` array",
|
||||||
conf: map[string]string{
|
conf: map[string]string{
|
||||||
"enable": "feature1,feature2",
|
"enable": "feature1,feature2",
|
||||||
},
|
},
|
||||||
expectedToggles: map[string]bool{
|
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||||
"feature1": true,
|
"feature1": {Key: "feature1", Variants: map[string]any{"": true}},
|
||||||
"feature2": true,
|
"feature2": {Key: "feature2", Variants: map[string]any{"": true}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -31,10 +30,10 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
"enable": "feature1,feature2",
|
"enable": "feature1,feature2",
|
||||||
"feature3": "true",
|
"feature3": "true",
|
||||||
},
|
},
|
||||||
expectedToggles: map[string]bool{
|
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||||
"feature1": true,
|
"feature1": {Key: "feature1", Variants: map[string]any{"": true}},
|
||||||
"feature2": true,
|
"feature2": {Key: "feature2", Variants: map[string]any{"": true}},
|
||||||
"feature3": true,
|
"feature3": {Key: "feature3", Variants: map[string]any{"": true}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,19 +42,26 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
"enable": "feature1,feature2",
|
"enable": "feature1,feature2",
|
||||||
"feature2": "false",
|
"feature2": "false",
|
||||||
},
|
},
|
||||||
expectedToggles: map[string]bool{
|
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||||
"feature1": true,
|
"feature1": {Key: "feature1", Variants: map[string]any{"": true}},
|
||||||
"feature2": false,
|
"feature2": {Key: "feature2", Variants: map[string]any{"": false}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid boolean value should return syntax error",
|
name: "type of the feature flag is handled correctly",
|
||||||
conf: map[string]string{
|
conf: map[string]string{
|
||||||
"enable": "feature1,feature2",
|
"feature1": "1", "feature2": "1.0",
|
||||||
"feature2": "invalid",
|
"feature3": `{"foo":"bar"}`, "feature4": "bar",
|
||||||
|
"feature5": "t", "feature6": "T",
|
||||||
|
},
|
||||||
|
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||||
|
"feature1": {Key: "feature1", Variants: map[string]any{"": 1}},
|
||||||
|
"feature2": {Key: "feature2", Variants: map[string]any{"": 1.0}},
|
||||||
|
"feature3": {Key: "feature3", Variants: map[string]any{"": map[string]any{"foo": "bar"}}},
|
||||||
|
"feature4": {Key: "feature4", Variants: map[string]any{"": "bar"}},
|
||||||
|
"feature5": {Key: "feature5", Variants: map[string]any{"": true}},
|
||||||
|
"feature6": {Key: "feature6", Variants: map[string]any{"": true}},
|
||||||
},
|
},
|
||||||
expectedToggles: map[string]bool{},
|
|
||||||
err: strconv.ErrSyntax,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,12 +75,11 @@ func TestFeatureToggles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
|
featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
|
||||||
require.ErrorIs(t, err, tc.err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if err == nil {
|
for k, v := range featureToggles {
|
||||||
for k, v := range featureToggles {
|
toggle := tc.expectedToggles[k]
|
||||||
require.Equal(t, tc.expectedToggles[k], v, tc.name)
|
require.Equal(t, toggle, v, tc.name)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -806,8 +806,10 @@ flowchart TD
|
|||||||
|
|
||||||
#### Setting Dual Writer Mode
|
#### Setting Dual Writer Mode
|
||||||
```ini
|
```ini
|
||||||
[unified_storage.{resource}.{kind}.{group}]
|
; [unified_storage.{resource}.{group}]
|
||||||
dualWriterMode = {0-5}
|
[unified_storage.dashboards.dashboard.grafana.app]
|
||||||
|
; modes {0-5}
|
||||||
|
dualWriterMode = 0
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Background Sync Configuration
|
#### Background Sync Configuration
|
||||||
@@ -1376,4 +1378,3 @@ disable_data_migrations = false
|
|||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md).
|
For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md).
|
||||||
|
|
||||||
@@ -44,6 +44,6 @@ func TestIntegrationFeatures(t *testing.T) {
|
|||||||
"value": true,
|
"value": true,
|
||||||
"key":"`+flag+`",
|
"key":"`+flag+`",
|
||||||
"reason":"static provider evaluation result",
|
"reason":"static provider evaluation result",
|
||||||
"variant":"enabled"}`, string(rsp.Body))
|
"variant":"default"}`, string(rsp.Body))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const getFormFieldsForSilence = (silence: Silence): SilenceFormFields =>
|
|||||||
startsAt: interval.start.toISOString(),
|
startsAt: interval.start.toISOString(),
|
||||||
endsAt: interval.end.toISOString(),
|
endsAt: interval.end.toISOString(),
|
||||||
comment: silence.comment,
|
comment: silence.comment,
|
||||||
createdBy: silence.createdBy,
|
createdBy: isExpired ? contextSrv.user.name : silence.createdBy,
|
||||||
duration: intervalToAbbreviatedDurationString(interval),
|
duration: intervalToAbbreviatedDurationString(interval),
|
||||||
isRegex: false,
|
isRegex: false,
|
||||||
matchers: silence.matchers?.map(matcherToMatcherField) || [],
|
matchers: silence.matchers?.map(matcherToMatcherField) || [],
|
||||||
|
|||||||
Reference in New Issue
Block a user