From 4407986e6c95b7bc0d0575b053c89ba4f26de803 Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Thu, 6 Nov 2025 11:51:31 +0000 Subject: [PATCH] feat(ofrep): add WithFromEnv() for environment variable configuration Add experimental WithFromEnv() option to configure the OFREP provider using environment variables. New features: - Add WithFromEnv() configuration option supporting: - OFREP_ENDPOINT: base URI for the OFREP service - OFREP_TIMEOUT: timeout duration (e.g., "30s", "500ms") - OFREP_API_KEY: API key for X-API-Key authentication - OFREP_BEARER_TOKEN: token for Bearer authentication - OFREP_HEADERS: comma-separated custom headers Signed-off-by: Roman Dmytrenko --- providers/ofrep/README.md | 19 ++++ providers/ofrep/provider.go | 45 ++++++++++ providers/ofrep/provider_test.go | 145 +++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+) diff --git a/providers/ofrep/README.md b/providers/ofrep/README.md index 1c7b14395..1babaeda3 100644 --- a/providers/ofrep/README.md +++ b/providers/ofrep/README.md @@ -38,6 +38,7 @@ You can configure the provider using following configuration options, | WithHeader | Set a custom header to be used for authorization | | WithBaseURI | Set the base URI of the OFREP service | | WithTimeout | Set the timeout for the http client used for communication with the OFREP service (ignored if custom client is used) | +| WithFromEnv | Configure the provider using environment variables (experimental) | For example, consider below example which sets bearer token and provides a customized http client, @@ -49,3 +50,21 @@ provider := ofrep.NewProvider( Timeout: 1 * time.Second, })) ``` + +### Environment Variable Configuration (Experimental) + +You can use the `WithFromEnv()` option to configure the provider using environment variables: + +```go +provider := ofrep.NewProvider( + "http://localhost:8016", + ofrep.WithFromEnv()) +``` + +Supported environment variables: + +| Environment Variable | Description | Example | +| -------------------- | --------------------------------------------------------------------- | ----------------------------------------- | +| OFREP_ENDPOINT | Base URI for the OFREP service (overrides the baseUri parameter) | `http://localhost:8016` | +| OFREP_TIMEOUT | Timeout duration for HTTP requests (ignored if custom client is used) | `30s`, `1m` or raw `5000` in milliseconds | +| OFREP_HEADERS | Comma-separated custom headers | `Key1=Value1,Key2=Value2` | diff --git a/providers/ofrep/provider.go b/providers/ofrep/provider.go index 7153ad076..3b70955a4 100644 --- a/providers/ofrep/provider.go +++ b/providers/ofrep/provider.go @@ -4,6 +4,9 @@ import ( "context" "fmt" "net/http" + "os" + "strconv" + "strings" "time" "github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/evaluate" @@ -126,3 +129,45 @@ func WithTimeout(timeout time.Duration) func(*outbound.Configuration) { c.Timeout = timeout } } + +// WithFromEnv uses environment variables to configure the provider. +// +// Experimental: This feature is experimental and may change in future versions. +// +// Supported environment variables: +// - OFREP_ENDPOINT: base URI for the OFREP service +// - OFREP_TIMEOUT: timeout duration (e.g., "30s", "1m" or raw "5000" in milliseconds ) +// - OFREP_HEADERS: comma-separated custom headers (e.g., "Key1=Value1,Key2=Value2") +func WithFromEnv() func(*outbound.Configuration) { + envHandlers := map[string]func(*outbound.Configuration, string){ + "OFREP_ENDPOINT": func(c *outbound.Configuration, v string) { + WithBaseURI(v)(c) + }, + "OFREP_TIMEOUT": func(c *outbound.Configuration, v string) { + if t, err := time.ParseDuration(v); err == nil && t > 0 { + WithTimeout(t)(c) + return + } + // as the specification is not finalized, also support raw milliseconds + t, err := strconv.Atoi(v) + if err == nil && t > 0 { + WithTimeout(time.Duration(t) * time.Millisecond)(c) + } + }, + "OFREP_HEADERS": func(c *outbound.Configuration, v string) { + for pair := range strings.SplitSeq(v, ",") { + kv := strings.SplitN(strings.TrimSpace(pair), "=", 2) + if len(kv) == 2 { + WithHeader(kv[0], kv[1])(c) + } + } + }, + } + return func(c *outbound.Configuration) { + for key, handler := range envHandlers { + if v := os.Getenv(key); v != "" { + handler(c, v) + } + } + } +} diff --git a/providers/ofrep/provider_test.go b/providers/ofrep/provider_test.go index 3e74f883a..902c468d3 100644 --- a/providers/ofrep/provider_test.go +++ b/providers/ofrep/provider_test.go @@ -147,3 +147,148 @@ func (r mockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { r.t.Logf("error wriging bytes: %v", err) } } + +func TestWithFromEnv(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + initialConfig outbound.Configuration + wantBaseURI string + wantTimeout time.Duration + wantHeaders map[string]string + }{ + { + name: "configure endpoint from env", + envVars: map[string]string{ + "OFREP_ENDPOINT": "http://test.example.com", + }, + initialConfig: outbound.Configuration{}, + wantBaseURI: "http://test.example.com", + }, + { + name: "configure timeout from env", + envVars: map[string]string{ + "OFREP_TIMEOUT": "5s", + }, + initialConfig: outbound.Configuration{}, + wantTimeout: 5 * time.Second, + }, + { + name: "configure timeout from env with raw milliseconds", + envVars: map[string]string{ + "OFREP_TIMEOUT": "3000", + }, + initialConfig: outbound.Configuration{}, + wantTimeout: 3 * time.Second, + }, + { + name: "configure timeout from env with invalid data", + envVars: map[string]string{ + "OFREP_TIMEOUT": "s5s", + }, + initialConfig: outbound.Configuration{Timeout: 33 * time.Second}, + wantTimeout: 33 * time.Second, + }, + { + name: "configure timeout from env with negative duration", + envVars: map[string]string{ + "OFREP_TIMEOUT": "-5s", + }, + initialConfig: outbound.Configuration{Timeout: 33 * time.Second}, + wantTimeout: 33 * time.Second, + }, + { + name: "configure timeout from env with negative milliseconds", + envVars: map[string]string{ + "OFREP_TIMEOUT": "-5000", + }, + initialConfig: outbound.Configuration{Timeout: 33 * time.Second}, + wantTimeout: 33 * time.Second, + }, + { + name: "ignore invalid timeout", + envVars: map[string]string{ + "OFREP_TIMEOUT": "invalid", + }, + initialConfig: outbound.Configuration{Timeout: 10 * time.Second}, + wantTimeout: 10 * time.Second, + }, + { + name: "configure custom headers from env", + envVars: map[string]string{ + "OFREP_HEADERS": "X-Custom-1=Value1,X-Custom-2=Value2", + }, + initialConfig: outbound.Configuration{}, + wantHeaders: map[string]string{ + "X-Custom-1": "Value1", + "X-Custom-2": "Value2", + }, + }, + { + name: "configure all options from env", + envVars: map[string]string{ + "OFREP_ENDPOINT": "http://all.example.com", + "OFREP_TIMEOUT": "3s", + "OFREP_HEADERS": "X-Test=TestValue", + }, + initialConfig: outbound.Configuration{}, + wantBaseURI: "http://all.example.com", + wantTimeout: 3 * time.Second, + wantHeaders: map[string]string{ + "X-Test": "TestValue", + }, + }, + { + name: "empty env variables do not override defaults", + envVars: map[string]string{ + "OFREP_ENDPOINT": "", + "OFREP_TIMEOUT": "", + }, + initialConfig: outbound.Configuration{ + BaseURI: "http://default.example.com", + Timeout: 15 * time.Second, + }, + wantBaseURI: "http://default.example.com", + wantTimeout: 15 * time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for key, value := range tt.envVars { + t.Setenv(key, value) + } + + c := tt.initialConfig + WithFromEnv()(&c) + + if tt.wantBaseURI != "" && c.BaseURI != tt.wantBaseURI { + t.Errorf("expected BaseURI %s, but got %s", tt.wantBaseURI, c.BaseURI) + } + + if tt.wantTimeout != 0 && c.Timeout != tt.wantTimeout { + t.Errorf("expected Timeout %v, but got %v", tt.wantTimeout, c.Timeout) + } + + actualHeaders := make(map[string]string) + for _, cb := range c.Callbacks { + k, v := cb() + actualHeaders[k] = v + } + + if tt.wantHeaders != nil { + for expectedKey, expectedValue := range tt.wantHeaders { + if actualValue, ok := actualHeaders[expectedKey]; !ok { + t.Errorf("expected header %s not found", expectedKey) + } else if actualValue != expectedValue { + t.Errorf("expected %s=%s, but got %s=%s", expectedKey, expectedValue, expectedKey, actualValue) + } + } + } + + if len(tt.wantHeaders) == 0 && len(actualHeaders) != 0 { + t.Errorf("expected no headers, but got %v", actualHeaders) + } + }) + } +}