Files
grafana/pkg/services/featuremgmt/openfeature.go
T
Denis Vodopianov ce9ab6a89a 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>
2026-01-12 22:53:23 +01:00

142 lines
4.5 KiB
Go

package featuremgmt
import (
"fmt"
"net/http"
"net/url"
"time"
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"
)
const (
featuresProviderAudience = "features.grafana.app"
)
// OpenFeatureConfig holds configuration for initializing OpenFeature
type OpenFeatureConfig struct {
// ProviderType is either "static", "features-service", or "ofrep"
ProviderType string
// URL is the remote provider's URL (required for features-service + OFREP providers)
URL *url.URL
// 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]memprovider.InMemoryFlag
// TargetingKey is used for evaluation context
TargetingKey string
// ContextAttrs are additional attributes for evaluation context
ContextAttrs map[string]any
}
// InitOpenFeature initializes OpenFeature with the provided configuration
func InitOpenFeature(config OpenFeatureConfig) error {
// For remote providers, ensure we have a URL
if (config.ProviderType == setting.FeaturesServiceProviderType || config.ProviderType == setting.OFREPProviderType) && (config.URL == nil || config.URL.String() == "") {
return fmt.Errorf("URL is required for remote providers")
}
p, err := createProvider(config.ProviderType, config.URL, config.StaticFlags, config.HTTPClient)
if err != nil {
return err
}
if err = openfeature.SetProviderAndWait(p); err != nil {
return fmt.Errorf("failed to set global feature provider: %s, %w", config.ProviderType, err)
}
contextAttrs := make(map[string]any)
for k, v := range config.ContextAttrs {
contextAttrs[k] = v
}
openfeature.SetEvaluationContext(openfeature.NewEvaluationContext(config.TargetingKey, contextAttrs))
return nil
}
// InitOpenFeatureWithCfg initializes OpenFeature from setting.Cfg
func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
if err != nil {
return fmt.Errorf("failed to read feature flags from config: %w", err)
}
var httpcli *http.Client
if cfg.OpenFeature.ProviderType == setting.FeaturesServiceProviderType || cfg.OpenFeature.ProviderType == setting.OFREPProviderType {
var m *clientauthmiddleware.TokenExchangeMiddleware
if cfg.OpenFeature.ProviderType == setting.FeaturesServiceProviderType {
m, err = clientauthmiddleware.NewTokenExchangeMiddleware(cfg)
if err != nil {
return fmt.Errorf("failed to create token exchange middleware: %w", err)
}
}
httpcli, err = createHTTPClient(m)
if err != nil {
return err
}
}
contextAttrs := make(map[string]any)
for k, v := range cfg.OpenFeature.ContextAttrs {
contextAttrs[k] = v
}
return InitOpenFeature(OpenFeatureConfig{
ProviderType: cfg.OpenFeature.ProviderType,
URL: cfg.OpenFeature.URL,
HTTPClient: httpcli,
StaticFlags: confFlags,
TargetingKey: cfg.OpenFeature.TargetingKey,
ContextAttrs: contextAttrs,
})
}
func createProvider(
providerType string,
u *url.URL,
staticFlags map[string]memprovider.InMemoryFlag,
httpClient *http.Client,
) (openfeature.FeatureProvider, error) {
if providerType == setting.FeaturesServiceProviderType || providerType == setting.OFREPProviderType {
if u == nil || u.String() == "" {
return nil, fmt.Errorf("feature provider url is required for FeaturesServiceProviderType + OFREPProviderType")
}
if providerType == setting.FeaturesServiceProviderType {
return newFeaturesServiceProvider(u.String(), httpClient)
}
if providerType == setting.OFREPProviderType {
return newOFREPProvider(u.String(), httpClient)
}
}
return newStaticProvider(staticFlags, standardFeatureFlags)
}
func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) {
options := sdkhttpclient.Options{
TLS: &sdkhttpclient.TLSOptions{InsecureSkipVerify: true},
Timeouts: &sdkhttpclient.TimeoutOptions{
Timeout: 10 * time.Second,
},
}
if m != nil {
options.Middlewares = append(options.Middlewares, m.New([]string{featuresProviderAudience}))
}
httpcli, err := sdkhttpclient.NewProvider().New(options)
if err != nil {
return nil, fmt.Errorf("failed to create http client for openfeature: %w", err)
}
return httpcli, nil
}