OpenFeature: Add OFREP provider type (#115857)

Add new OFREP provider for OpenFeature
This commit is contained in:
Matt Cowley
2026-01-06 14:39:07 +00:00
committed by GitHub
parent 7698970f22
commit 264131a390
6 changed files with 75 additions and 32 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
}
@@ -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
}
+35 -24
View File
@@ -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)
}
+18 -4
View File
@@ -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")
}
+2 -1
View File
@@ -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)