Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions providers/ofrep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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` |
45 changes: 45 additions & 0 deletions providers/ofrep/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/evaluate"
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the cloud native approach of using EnvVar, but I think we should apply this by default. So when a user decides to set this, it will utilize those. The benefit is, that you can set this without a deployment and the usage of this variable.

The usual cloudnative approach is EnvVar -> setting -> default. https://github.com/open-feature/flagd-testbed/blob/main/gherkin/config.feature out lines this via gherkin files.

This is just a suggestion, not sure if this approach has been discussed or not :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aepfli Could you please clarify what you mean? Should I append WithFromEnv() to the options when creating the OFREP provider?

Right now this is just a building block, so people can opt in if they want to use it like this

	ofrep.NewProvider("https://default.endpoint", ofrep.WithFromEnv())

This is similar to the OTEL approach. https://pkg.go.dev/go.opentelemetry.io/otel/sdk/resource#WithFromEnv

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erka - based on the information provided I am now torn ;) - the cloud native way or at least how i interpret it (@toddbaert correct me if i am wrong) would be to append it, or do it automagically, to fallback to the env variables if there is not dedicated 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)
}
}
}
}
145 changes: 145 additions & 0 deletions providers/ofrep/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
Loading