Extend OpenFeature service (#106707)
This commit is contained in:
@@ -1,71 +1,112 @@
|
|||||||
package featuremgmt
|
package featuremgmt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
staticProviderType = "static"
|
|
||||||
goffProviderType = "goff"
|
|
||||||
|
|
||||||
configSectionName = "feature_toggles.openfeature"
|
|
||||||
contextSectionName = "feature_toggles.openfeature.context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OpenFeatureService struct {
|
type OpenFeatureService struct {
|
||||||
|
cfg *setting.Cfg
|
||||||
|
log log.Logger
|
||||||
provider openfeature.FeatureProvider
|
provider openfeature.FeatureProvider
|
||||||
Client openfeature.IClient
|
Client openfeature.IClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideOpenFeatureService(cfg *setting.Cfg) (*OpenFeatureService, error) {
|
func ProvideOpenFeatureService(cfg *setting.Cfg) (*OpenFeatureService, error) {
|
||||||
conf := cfg.Raw.Section(configSectionName)
|
|
||||||
provType := conf.Key("provider").MustString(staticProviderType)
|
|
||||||
url := conf.Key("url").MustString("")
|
|
||||||
key := conf.Key("targetingKey").MustString(cfg.AppURL)
|
|
||||||
|
|
||||||
var provider openfeature.FeatureProvider
|
var provider openfeature.FeatureProvider
|
||||||
var err error
|
var err error
|
||||||
if provType == goffProviderType {
|
if cfg.OpenFeature.ProviderType == setting.GOFFProviderType {
|
||||||
provider, err = newGOFFProvider(url)
|
if cfg.OpenFeature.URL == nil {
|
||||||
|
return nil, fmt.Errorf("feature provider url is required for GOFFProviderType")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err = newGOFFProvider(cfg.OpenFeature.URL.String())
|
||||||
} else {
|
} else {
|
||||||
provider, err = newStaticProvider(cfg)
|
provider, err = newStaticProvider(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create %s feature provider: %w", provType, err)
|
return nil, fmt.Errorf("failed to create %s feature provider: %w", cfg.OpenFeature.ProviderType, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := openfeature.SetProviderAndWait(provider); err != nil {
|
if err := openfeature.SetProviderAndWait(provider); err != nil {
|
||||||
return nil, fmt.Errorf("failed to set global %s feature provider: %w", provType, err)
|
return nil, fmt.Errorf("failed to set global %s feature provider: %w", cfg.OpenFeature.ProviderType, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs := ctxAttrs(cfg)
|
openfeature.SetEvaluationContext(openfeature.NewEvaluationContext(cfg.OpenFeature.TargetingKey, cfg.OpenFeature.ContextAttrs))
|
||||||
openfeature.SetEvaluationContext(openfeature.NewEvaluationContext(key, attrs))
|
|
||||||
|
|
||||||
client := openfeature.NewClient("grafana-openfeature-client")
|
client := openfeature.NewClient("grafana-openfeature-client")
|
||||||
|
|
||||||
return &OpenFeatureService{
|
return &OpenFeatureService{
|
||||||
|
cfg: cfg,
|
||||||
|
log: log.New("openfeatureservice"),
|
||||||
provider: provider,
|
provider: provider,
|
||||||
Client: client,
|
Client: client,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ctxAttrs uses config.ini [feature_toggles.openfeature.context] section to build the eval context attributes
|
func (s *OpenFeatureService) EvalFlagWithStaticProvider(ctx context.Context, flagKey string) (openfeature.BooleanEvaluationDetails, error) {
|
||||||
func ctxAttrs(cfg *setting.Cfg) map[string]any {
|
_, ok := s.provider.(*inMemoryBulkProvider)
|
||||||
ctxConf := cfg.Raw.Section(contextSectionName)
|
if !ok {
|
||||||
|
return openfeature.BooleanEvaluationDetails{}, fmt.Errorf("not a static provider, request must be sent to open feature service")
|
||||||
attrs := map[string]any{}
|
|
||||||
for _, key := range ctxConf.KeyStrings() {
|
|
||||||
attrs[key] = ctxConf.Key(key).String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some default attributes
|
result, err := s.Client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
|
||||||
if _, ok := attrs["grafana_version"]; !ok {
|
if err != nil {
|
||||||
attrs["grafana_version"] = setting.BuildVersion
|
return openfeature.BooleanEvaluationDetails{}, fmt.Errorf("failed to evaluate flag %s: %w", flagKey, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrs
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OpenFeatureService) EvalAllFlagsWithStaticProvider(ctx context.Context) (OFREPBulkResponse, error) {
|
||||||
|
p, ok := s.provider.(*inMemoryBulkProvider)
|
||||||
|
if !ok {
|
||||||
|
return OFREPBulkResponse{}, fmt.Errorf("not a static provider, request must be sent to open feature service")
|
||||||
|
}
|
||||||
|
|
||||||
|
flags, err := p.ListFlags()
|
||||||
|
if err != nil {
|
||||||
|
return OFREPBulkResponse{}, fmt.Errorf("static provider failed to list all flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allFlags := make([]OFREPFlag, 0, len(flags))
|
||||||
|
for _, flagKey := range flags {
|
||||||
|
result, err := s.Client.BooleanValueDetails(ctx, flagKey, false, openfeature.TransactionContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to evaluate flag during bulk evaluation", "flagKey", flagKey, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
allFlags = append(allFlags, OFREPFlag{
|
||||||
|
Key: flagKey,
|
||||||
|
Value: result.Value,
|
||||||
|
Reason: "static provider evaluation result",
|
||||||
|
Variant: result.Variant,
|
||||||
|
ErrorCode: string(result.ErrorCode),
|
||||||
|
ErrorDetails: result.ErrorMessage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return OFREPBulkResponse{Flags: allFlags}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk evaluation response
|
||||||
|
type OFREPBulkResponse struct {
|
||||||
|
Flags []OFREPFlag `json:"flags"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OFREPFlag struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value bool `json:"value"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Variant string `json:"variant,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
|
ErrorCode string `json:"errorCode,omitempty"`
|
||||||
|
ErrorDetails string `json:"errorDetails,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +1,62 @@
|
|||||||
package featuremgmt
|
package featuremgmt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg"
|
gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProvideOpenFeatureManager(t *testing.T) {
|
func TestProvideOpenFeatureManager(t *testing.T) {
|
||||||
|
u, err := url.Parse("http://localhost:1031")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
cfg string
|
cfg setting.OpenFeatureSettings
|
||||||
expectedProvider string
|
expectedProvider string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "static provider",
|
name: "static provider",
|
||||||
expectedProvider: staticProviderType,
|
expectedProvider: setting.StaticProviderType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "goff provider",
|
name: "goff provider",
|
||||||
cfg: `
|
cfg: setting.OpenFeatureSettings{
|
||||||
[feature_toggles.openfeature]
|
ProviderType: setting.GOFFProviderType,
|
||||||
provider = goff
|
URL: u,
|
||||||
url = http://localhost:1031
|
TargetingKey: "grafana",
|
||||||
targetingKey = grafana
|
},
|
||||||
`,
|
expectedProvider: setting.GOFFProviderType,
|
||||||
expectedProvider: goffProviderType,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid provider",
|
name: "invalid provider",
|
||||||
cfg: `
|
cfg: setting.OpenFeatureSettings{
|
||||||
[feature_toggles.openfeature]
|
ProviderType: "some_provider",
|
||||||
provider = some_provider
|
},
|
||||||
`,
|
expectedProvider: setting.StaticProviderType,
|
||||||
expectedProvider: staticProviderType,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
if tc.cfg != "" {
|
cfg.OpenFeature = tc.cfg
|
||||||
err := cfg.Raw.Append([]byte(tc.cfg))
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := ProvideOpenFeatureService(cfg)
|
p, err := ProvideOpenFeatureService(cfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if tc.expectedProvider == goffProviderType {
|
if tc.expectedProvider == setting.GOFFProviderType {
|
||||||
_, ok := p.provider.(*gofeatureflag.Provider)
|
_, ok := p.provider.(*gofeatureflag.Provider)
|
||||||
assert.True(t, ok, "expected provider to be of type goff.Provider")
|
assert.True(t, ok, "expected provider to be of type goff.Provider")
|
||||||
} else {
|
} else {
|
||||||
_, ok := p.provider.(memprovider.InMemoryProvider)
|
_, ok := p.provider.(*inMemoryBulkProvider)
|
||||||
assert.True(t, ok, "expected provider to be of type memprovider.InMemoryProvider")
|
assert.True(t, ok, "expected provider to be of type memprovider.InMemoryProvider")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_CtxAttrs(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
conf string
|
|
||||||
expected map[string]any
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty config - only default attributes should be present",
|
|
||||||
expected: map[string]any{
|
|
||||||
"grafana_version": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "config with some attributes",
|
|
||||||
conf: `
|
|
||||||
[feature_toggles.openfeature.context]
|
|
||||||
foo = bar
|
|
||||||
baz = qux
|
|
||||||
quux = corge`,
|
|
||||||
expected: map[string]any{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": "qux",
|
|
||||||
"quux": "corge",
|
|
||||||
"grafana_version": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "config with an attribute that overrides a default one",
|
|
||||||
conf: `
|
|
||||||
[feature_toggles.openfeature.context]
|
|
||||||
grafana_version = 10.0.0
|
|
||||||
foo = bar`,
|
|
||||||
expected: map[string]any{
|
|
||||||
"grafana_version": "10.0.0",
|
|
||||||
"foo": "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
cfg, err := setting.NewCfgFromBytes([]byte(tc.conf))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, tc.expected, ctxAttrs(cfg))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,29 @@ import (
|
|||||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that
|
||||||
|
// also allows for bulk evaluation of flags, necessary to proxy OFREP requests.
|
||||||
|
type inMemoryBulkProvider struct {
|
||||||
|
memprovider.InMemoryProvider
|
||||||
|
flags map[string]memprovider.InMemoryFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInMemoryBulkProvider(flags map[string]memprovider.InMemoryFlag) *inMemoryBulkProvider {
|
||||||
|
return &inMemoryBulkProvider{
|
||||||
|
InMemoryProvider: memprovider.NewInMemoryProvider(flags),
|
||||||
|
flags: flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFlags returns a list of all flags registered with the provider.
|
||||||
|
func (p *inMemoryBulkProvider) ListFlags() ([]string, error) {
|
||||||
|
keys := make([]string, 0, len(p.flags))
|
||||||
|
for key := range p.flags {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
func newStaticProvider(cfg *setting.Cfg) (openfeature.FeatureProvider, error) {
|
func newStaticProvider(cfg *setting.Cfg) (openfeature.FeatureProvider, error) {
|
||||||
confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -29,7 +52,7 @@ func newStaticProvider(cfg *setting.Cfg) (openfeature.FeatureProvider, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return memprovider.NewInMemoryProvider(flags), nil
|
return newInMemoryBulkProvider(flags), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag {
|
func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag {
|
||||||
|
|||||||
@@ -56,3 +56,37 @@ func provider(t *testing.T, conf []byte) *OpenFeatureService {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_CompareStaticProviderWithFeatureManager(t *testing.T) {
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
sec, err := cfg.Raw.NewSection("feature_toggles")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = sec.NewKey("ABCD", "true")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
p, err := ProvideOpenFeatureService(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, ok := p.provider.(*inMemoryBulkProvider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected inMemoryBulkProvider, got %T", p.provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := openfeature.WithTransactionContext(context.Background(), openfeature.NewEvaluationContext("grafana", nil))
|
||||||
|
allFlags, err := p.EvalAllFlagsWithStaticProvider(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
openFeatureEnabledFlags := map[string]bool{}
|
||||||
|
for _, flag := range allFlags.Flags {
|
||||||
|
if flag.Value {
|
||||||
|
openFeatureEnabledFlags[flag.Key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := ProvideManagerService(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// compare enabled feature flags match between OpenFeature static provider and Feature Manager
|
||||||
|
enabledFeatureManager := mgr.GetEnabled(ctx)
|
||||||
|
assert.Equal(t, openFeatureEnabledFlags, enabledFeatureManager)
|
||||||
|
}
|
||||||
|
|||||||
@@ -476,6 +476,9 @@ type Cfg struct {
|
|||||||
// Query history
|
// Query history
|
||||||
QueryHistoryEnabled bool
|
QueryHistoryEnabled bool
|
||||||
|
|
||||||
|
// Open feature settings
|
||||||
|
OpenFeature OpenFeatureSettings
|
||||||
|
|
||||||
Storage StorageSettings
|
Storage StorageSettings
|
||||||
|
|
||||||
Search SearchSettings
|
Search SearchSettings
|
||||||
@@ -1319,6 +1322,11 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := cfg.readOpenFeatureSettings(); err != nil {
|
||||||
|
cfg.Logger.Error("Failed to read open feature settings", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
cfg.readDataSourcesSettings()
|
cfg.readDataSourcesSettings()
|
||||||
cfg.readDataSourceSecuritySettings()
|
cfg.readDataSourceSecuritySettings()
|
||||||
cfg.readK8sDashboardCleanupSettings()
|
cfg.readK8sDashboardCleanupSettings()
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StaticProviderType = "static"
|
||||||
|
GOFFProviderType = "goff"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenFeatureSettings struct {
|
||||||
|
ProviderType string
|
||||||
|
URL *url.URL
|
||||||
|
TargetingKey string
|
||||||
|
ContextAttrs map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Cfg) readOpenFeatureSettings() error {
|
||||||
|
cfg.OpenFeature = OpenFeatureSettings{}
|
||||||
|
|
||||||
|
config := cfg.Raw.Section("feature_toggles.openfeature")
|
||||||
|
cfg.OpenFeature.ProviderType = config.Key("provider").MustString(StaticProviderType)
|
||||||
|
cfg.OpenFeature.TargetingKey = config.Key("targetingKey").MustString(cfg.AppURL)
|
||||||
|
|
||||||
|
strURL := config.Key("url").MustString("")
|
||||||
|
|
||||||
|
if strURL != "" && cfg.OpenFeature.ProviderType == GOFFProviderType {
|
||||||
|
u, err := url.Parse(strURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid feature provider url: %w", err)
|
||||||
|
}
|
||||||
|
cfg.OpenFeature.URL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the eval context attributes using [feature_toggles.openfeature.context] section
|
||||||
|
ctxConf := cfg.Raw.Section("feature_toggles.openfeature.context")
|
||||||
|
attrs := map[string]any{}
|
||||||
|
for _, key := range ctxConf.KeyStrings() {
|
||||||
|
attrs[key] = ctxConf.Key(key).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some default attributes
|
||||||
|
if _, ok := attrs["grafana_version"]; !ok {
|
||||||
|
attrs["grafana_version"] = BuildVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.OpenFeature.ContextAttrs = attrs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_CtxAttrs(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
conf string
|
||||||
|
expected map[string]any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty config - only default attributes should be present",
|
||||||
|
expected: map[string]any{
|
||||||
|
"grafana_version": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with some attributes",
|
||||||
|
conf: `
|
||||||
|
[feature_toggles.openfeature.context]
|
||||||
|
foo = bar
|
||||||
|
baz = qux
|
||||||
|
quux = corge`,
|
||||||
|
expected: map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "qux",
|
||||||
|
"quux": "corge",
|
||||||
|
"grafana_version": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with an attribute that overrides a default one",
|
||||||
|
conf: `
|
||||||
|
[feature_toggles.openfeature.context]
|
||||||
|
grafana_version = 10.0.0
|
||||||
|
foo = bar`,
|
||||||
|
expected: map[string]any{
|
||||||
|
"grafana_version": "10.0.0",
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cfg, err := NewCfgFromBytes([]byte(tc.conf))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected, cfg.OpenFeature.ContextAttrs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user