diff --git a/go.mod b/go.mod index bba74f9b656..6df4d73089e 100644 --- a/go.mod +++ b/go.mod @@ -147,6 +147,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group github.com/open-feature/go-sdk v1.16.0 // @grafana/grafana-backend-group github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // @grafana/grafana-backend-group + github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // @grafana/grafana-backend-group github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 // @grafana/identity-access-team github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c // @grafana/identity-access-team github.com/openfga/openfga v1.11.1 // @grafana/identity-access-team @@ -544,7 +545,6 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect - github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/core/xidutils v0.124.1 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 // indirect diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index d6a443cfee7..3dcc6a36adc 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -276,7 +276,7 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) { return } - if b.providerType == setting.GOFFProviderType { + if b.providerType == setting.GOFFProviderType || b.providerType == setting.OFREPProviderType { b.proxyFlagReq(ctx, flagKey, isAuthedReq, w, r) return } @@ -304,7 +304,7 @@ func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) { isAuthedReq := b.isAuthenticatedRequest(r) span.SetAttributes(attribute.Bool("authenticated", isAuthedReq)) - if b.providerType == setting.GOFFProviderType { + if b.providerType == setting.GOFFProviderType || b.providerType == setting.OFREPProviderType { b.proxyAllFlagReq(ctx, isAuthedReq, w, r) return } diff --git a/pkg/services/featuremgmt/ofrep_provider.go b/pkg/services/featuremgmt/ofrep_provider.go new file mode 100644 index 00000000000..a5197603db4 --- /dev/null +++ b/pkg/services/featuremgmt/ofrep_provider.go @@ -0,0 +1,17 @@ +package featuremgmt + +import ( + "net/http" + + ofrep "github.com/open-feature/go-sdk-contrib/providers/ofrep" + "github.com/open-feature/go-sdk/openfeature" +) + +func newOFREPProvider(url string, client *http.Client) (openfeature.FeatureProvider, error) { + options := []ofrep.Option{} + if client != nil { + options = append(options, ofrep.WithClient(client)) + } + + return ofrep.NewProvider(url, options...), nil +} diff --git a/pkg/services/featuremgmt/openfeature.go b/pkg/services/featuremgmt/openfeature.go index 234739ee59f..a904107bfbd 100644 --- a/pkg/services/featuremgmt/openfeature.go +++ b/pkg/services/featuremgmt/openfeature.go @@ -19,11 +19,11 @@ const ( // OpenFeatureConfig holds configuration for initializing OpenFeature type OpenFeatureConfig struct { - // ProviderType is either "static" or "goff" + // ProviderType is either "static", "goff", or "ofrep" ProviderType string - // URL is the GOFF service URL (required for GOFF provider) + // URL is the GOFF or OFREP service URL (required for GOFF + OFREP providers) URL *url.URL - // HTTPClient is a pre-configured HTTP client (optional, used for GOFF provider) + // HTTPClient is a pre-configured HTTP client (optional, used for GOFF + OFREP providers) HTTPClient *http.Client // StaticFlags are the feature flags to use with static provider StaticFlags map[string]bool @@ -35,9 +35,9 @@ type OpenFeatureConfig struct { // 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") + // For GOFF + OFREP providers, ensure we have a URL + if (config.ProviderType == setting.GOFFProviderType || config.ProviderType == setting.OFREPProviderType) && (config.URL == nil || config.URL.String() == "") { + return fmt.Errorf("URL is required for GOFF + OFREP providers") } p, err := createProvider(config.ProviderType, config.URL, config.StaticFlags, config.HTTPClient) @@ -66,13 +66,17 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error { } 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) + if cfg.OpenFeature.ProviderType == setting.GOFFProviderType || cfg.OpenFeature.ProviderType == setting.OFREPProviderType { + var m *clientauthmiddleware.TokenExchangeMiddleware + + 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) + httpcli, err = createHTTPClient(m) if err != nil { return err } @@ -99,28 +103,35 @@ func createProvider( staticFlags map[string]bool, httpClient *http.Client, ) (openfeature.FeatureProvider, error) { - if providerType != setting.GOFFProviderType { - return newStaticProvider(staticFlags) + if providerType == setting.GOFFProviderType || providerType == setting.OFREPProviderType { + if u == nil || u.String() == "" { + return nil, fmt.Errorf("feature provider url is required for GOFFProviderType + OFREPProviderType") + } + + if providerType == setting.GOFFProviderType { + return newGOFFProvider(u.String(), httpClient) + } + + if providerType == setting.OFREPProviderType { + return newOFREPProvider(u.String(), httpClient) + } } - if u == nil || u.String() == "" { - return nil, fmt.Errorf("feature provider url is required for GOFFProviderType") - } - - return newGOFFProvider(u.String(), httpClient) + return newStaticProvider(staticFlags) } -func goffHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) { - httpcli, err := sdkhttpclient.NewProvider().New(sdkhttpclient.Options{ +func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) { + options := sdkhttpclient.Options{ TLS: &sdkhttpclient.TLSOptions{InsecureSkipVerify: true}, Timeouts: &sdkhttpclient.TimeoutOptions{ Timeout: 10 * time.Second, }, - Middlewares: []sdkhttpclient.Middleware{ - m.New([]string{featuresProviderAudience}), - }, - }) + } + 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) } diff --git a/pkg/services/featuremgmt/openfeature_test.go b/pkg/services/featuremgmt/openfeature_test.go index 788b53531d4..152a807f8dc 100644 --- a/pkg/services/featuremgmt/openfeature_test.go +++ b/pkg/services/featuremgmt/openfeature_test.go @@ -7,6 +7,7 @@ import ( "testing" gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg" + ofrep "github.com/open-feature/go-sdk-contrib/providers/ofrep" "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -60,6 +61,15 @@ func TestCreateProvider(t *testing.T) { expectedProvider: setting.GOFFProviderType, failSigning: true, }, + { + name: "ofrep provider", + cfg: setting.OpenFeatureSettings{ + ProviderType: setting.OFREPProviderType, + URL: u, + TargetingKey: "grafana", + }, + expectedProvider: setting.OFREPProviderType, + }, { name: "invalid provider", cfg: setting.OpenFeatureSettings{ @@ -96,20 +106,24 @@ func TestCreateProvider(t *testing.T) { } tokenExchangeMiddleware := middleware.TestingTokenExchangeMiddleware(tokenExchangeClient) - goffClient, err := goffHTTPClient(tokenExchangeMiddleware) + httpClient, err := createHTTPClient(tokenExchangeMiddleware) require.NoError(t, err, "failed to create goff http client") - provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil, goffClient) + provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil, httpClient) require.NoError(t, err) err = openfeature.SetProviderAndWait(provider) require.NoError(t, err, "failed to set provider") - if tc.expectedProvider == setting.GOFFProviderType { + switch tc.expectedProvider { + case setting.GOFFProviderType: _, ok := provider.(*gofeatureflag.Provider) assert.True(t, ok, "expected provider to be of type goff.Provider") testGoFFProvider(t, tc.failSigning) - } else { + case setting.OFREPProviderType: + _, ok := provider.(*ofrep.Provider) + assert.True(t, ok, "expected provider to be of type ofrep.Provider") + default: _, ok := provider.(*inMemoryBulkProvider) assert.True(t, ok, "expected provider to be of type memprovider.InMemoryProvider") } diff --git a/pkg/setting/setting_openfeature.go b/pkg/setting/setting_openfeature.go index 52966b0f7ba..16eaa72e55c 100644 --- a/pkg/setting/setting_openfeature.go +++ b/pkg/setting/setting_openfeature.go @@ -8,6 +8,7 @@ import ( const ( StaticProviderType = "static" GOFFProviderType = "goff" + OFREPProviderType = "ofrep" ) type OpenFeatureSettings struct { @@ -33,7 +34,7 @@ func (cfg *Cfg) readOpenFeatureSettings() error { cfg.OpenFeature.TargetingKey = config.Key("targetingKey").MustString(defaultTargetingKey) - if strURL != "" && cfg.OpenFeature.ProviderType == GOFFProviderType { + if strURL != "" && (cfg.OpenFeature.ProviderType == GOFFProviderType || cfg.OpenFeature.ProviderType == OFREPProviderType) { u, err := url.Parse(strURL) if err != nil { return fmt.Errorf("invalid feature provider url: %w", err)