package featuremgmt import ( "fmt" "net/http" "net/url" "time" clientauthmiddleware "github.com/grafana/grafana/pkg/clientauth/middleware" "github.com/grafana/grafana/pkg/setting" 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" or "goff" ProviderType string // URL is the GOFF service URL (required for GOFF provider) URL *url.URL // HTTPClient is a pre-configured HTTP client (optional, used for GOFF provider) HTTPClient *http.Client // StaticFlags are the feature flags to use with static provider StaticFlags map[string]bool // 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 GOFF provider, ensure we have a URL if config.ProviderType == setting.GOFFProviderType && (config.URL == nil || config.URL.String() == "") { return fmt.Errorf("URL is required for GOFF provider") } 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.GOFFProviderType { m, err := clientauthmiddleware.NewTokenExchangeMiddleware(cfg) if err != nil { return fmt.Errorf("failed to create token exchange middleware: %w", err) } httpcli, err = goffHTTPClient(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]bool, httpClient *http.Client, ) (openfeature.FeatureProvider, error) { if providerType != setting.GOFFProviderType { return newStaticProvider(staticFlags) } if u == nil || u.String() == "" { return nil, fmt.Errorf("feature provider url is required for GOFFProviderType") } return newGOFFProvider(u.String(), httpClient) } func goffHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) { httpcli, err := sdkhttpclient.NewProvider().New(sdkhttpclient.Options{ TLS: &sdkhttpclient.TLSOptions{InsecureSkipVerify: true}, Timeouts: &sdkhttpclient.TimeoutOptions{ Timeout: 10 * time.Second, }, Middlewares: []sdkhttpclient.Middleware{ m.New([]string{featuresProviderAudience}), }, }) if err != nil { return nil, fmt.Errorf("failed to create http client for openfeature: %w", err) } return httpcli, nil }