Skip to content

Commit 86d0d79

Browse files
committed
feat(ofrep): add environment variable configuration and improve provider
Add experimental WithFromEnv() option to configure the OFREP provider using environment variables, along with several enhancements and improvements. 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 - Add WithHeader() option for setting custom headers - Add WithBaseURI() option to override base URI - Add WithTimeout() option to configure HTTP client timeout - Set Content-Type header to "application/json" in HTTP requests - Update error handling to include underlying error causes - Add Go SDK interface assertion (var _ openfeature.FeatureProvider) Signed-off-by: Roman Dmytrenko <[email protected]>
1 parent 6269328 commit 86d0d79

File tree

8 files changed

+292
-24
lines changed

8 files changed

+292
-24
lines changed

providers/ofrep/README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@ You can configure the provider using following configuration options,
3434
|----------------------|-------------------------------------------------------------------------------------------------------------------------|
3535
| WithApiKeyAuth | Set the token to be used with "X-API-Key" header |
3636
| WithBearerToken | Set the token to be used with "Bearer" HTTP Authorization schema |
37-
| WithClient | Provider a custom, pre-configured http.Client for OFREP service communication |
37+
| WithClient | Provide a custom, pre-configured http.Client for OFREP service communication |
38+
| WithHeader | Set a custom header to be used for authorization |
3839
| WithHeaderProvider | Register a custom header provider for OFREP calls. You may utilize this for custom authentication/authorization headers |
40+
| WithBaseURI | Set the base URI of the OFREP service |
41+
| WithTimeout | Set the timeout for the http client used for communication with the OFREP service (ignored if custom client is used) |
42+
| WithFromEnv | Configure the provider using environment variables (experimental) |
3943

4044

41-
For example, consider below example which set bearer token and provider a customized http client,
45+
For example, consider below example which sets bearer token and provides a customized http client,
4246

4347
```go
4448
provider := ofrep.NewProvider(
@@ -47,4 +51,24 @@ provider := ofrep.NewProvider(
4751
ofrep.WithClient(&http.Client{
4852
Timeout: 1 * time.Second,
4953
}))
50-
```
54+
```
55+
56+
### Environment Variable Configuration (Experimental)
57+
58+
You can use the `WithFromEnv()` option to configure the provider using environment variables:
59+
60+
```go
61+
provider := ofrep.NewProvider(
62+
"http://localhost:8016",
63+
ofrep.WithFromEnv())
64+
```
65+
66+
Supported environment variables:
67+
68+
| Environment Variable | Description | Example |
69+
|---------------------|------------------------------------------------------------------|----------------------------------|
70+
| OFREP_ENDPOINT | Base URI for the OFREP service (overrides the baseUri parameter) | `http://localhost:8016` |
71+
| OFREP_TIMEOUT | Timeout duration for HTTP requests (ignored if custom client is used) | `30s`, `500ms` |
72+
| OFREP_API_KEY | API key for X-API-Key authentication | `your-api-key` |
73+
| OFREP_BEARER_TOKEN | Token for Bearer authentication | `your-bearer-token` |
74+
| OFREP_HEADERS | Comma-separated custom headers | `Key1=Value1,Key2=Value2` |

providers/ofrep/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module github.com/open-feature/go-sdk-contrib/providers/ofrep
22

33
go 1.24.0
44

5-
require github.com/open-feature/go-sdk v1.16.0
5+
require github.com/open-feature/go-sdk v1.17.0
66

77
require (
88
github.com/go-logr/logr v1.4.3 // indirect

providers/ofrep/go.sum

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
22
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3-
github.com/open-feature/go-sdk v1.15.1 h1:TC3FtHtOKlGlIbSf3SEpxXVhgTd/bCbuc39XHIyltkw=
4-
github.com/open-feature/go-sdk v1.15.1/go.mod h1:2WAFYzt8rLYavcubpCoiym3iSCXiHdPB6DxtMkv2wyo=
5-
github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
6-
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
7-
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
8-
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
3+
github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk=
4+
github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw=
95
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
106
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
11-
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
12-
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
7+
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
8+
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=

providers/ofrep/internal/evaluate/flags_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func genericValidator[T knownTypes](test testDefinition[T], resolvedValue T, rea
359359
}
360360

361361
if !reflect.DeepEqual(test.defaultValue, resolvedValue) {
362-
t.Errorf("expected deafault value %v, but got %v", test.defaultValue, resolvedValue)
362+
t.Errorf("expected default value %v, but got %v", test.defaultValue, resolvedValue)
363363
}
364364

365365
if reason != of.ErrorReason {

providers/ofrep/internal/evaluate/resolver.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ func NewOutboundResolver(cfg outbound.Configuration) *OutboundResolver {
3131
func (g *OutboundResolver) resolveSingle(ctx context.Context, key string, evalCtx map[string]any) (*successDto, *of.ResolutionError) {
3232
b, err := json.Marshal(requestFrom(evalCtx))
3333
if err != nil {
34-
resErr := of.NewGeneralResolutionError(fmt.Sprintf("context marshelling error: %v", err))
34+
resErr := of.NewGeneralResolutionError(fmt.Sprintf("context marshelling error: %v", err), err)
3535
return nil, &resErr
3636
}
3737

3838
rsp, err := g.client.Single(ctx, key, b)
3939
if err != nil {
40-
resErr := of.NewGeneralResolutionError(fmt.Sprintf("ofrep request error: %v", err))
40+
resErr := of.NewGeneralResolutionError(fmt.Sprintf("ofrep request error: %v", err), err)
4141
return nil, &resErr
4242
}
4343

@@ -47,7 +47,7 @@ func (g *OutboundResolver) resolveSingle(ctx context.Context, key string, evalCt
4747
var success evaluationSuccess
4848
err := json.Unmarshal(rsp.Data, &success)
4949
if err != nil {
50-
resErr := of.NewParseErrorResolutionError(fmt.Sprintf("error parsing the response: %v", err))
50+
resErr := of.NewParseErrorResolutionError(fmt.Sprintf("error parsing the response: %v", err), err)
5151
return nil, &resErr
5252
}
5353
return toSuccessDto(success)
@@ -82,7 +82,7 @@ func parseError400(data []byte) *of.ResolutionError {
8282
var evalError evaluationError
8383
err := json.Unmarshal(data, &evalError)
8484
if err != nil {
85-
resErr := of.NewGeneralResolutionError(fmt.Sprintf("error parsing error payload: %v", err))
85+
resErr := of.NewGeneralResolutionError(fmt.Sprintf("error parsing error payload: %v", err), err)
8686
return &resErr
8787
}
8888

@@ -127,7 +127,7 @@ func parseError500(data []byte) *of.ResolutionError {
127127

128128
err := json.Unmarshal(data, &evalError)
129129
if err != nil {
130-
resErr = of.NewGeneralResolutionError(fmt.Sprintf("error parsing error payload: %v", err))
130+
resErr = of.NewGeneralResolutionError(fmt.Sprintf("error parsing error payload: %v", err), err)
131131
} else {
132132
resErr = of.NewGeneralResolutionError(evalError.ErrorDetails)
133133
}

providers/ofrep/internal/outbound/http.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type HeaderCallback func() (name string, value string)
1919

2020
type Configuration struct {
2121
BaseURI string
22+
Timeout time.Duration
2223
Callbacks []HeaderCallback
2324
Client *http.Client
2425
}
@@ -39,7 +40,7 @@ type Outbound struct {
3940
func NewHttp(cfg Configuration) *Outbound {
4041
if cfg.Client == nil {
4142
cfg.Client = &http.Client{
42-
Timeout: 10 * time.Second,
43+
Timeout: cfg.Timeout,
4344
}
4445
}
4546

@@ -62,6 +63,8 @@ func (h *Outbound) Single(ctx context.Context, key string, payload []byte) (*Res
6263
return nil, &resErr
6364
}
6465

66+
req.Header.Set("Content-Type", "application/json")
67+
6568
for _, callback := range h.headerProvider {
6669
req.Header.Set(callback())
6770
}

providers/ofrep/provider.go

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"os"
8+
"strconv"
9+
"strings"
10+
"time"
711

812
"github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/evaluate"
913
"github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/outbound"
1014
"github.com/open-feature/go-sdk/openfeature"
1115
)
1216

17+
var _ openfeature.FeatureProvider = (*Provider)(nil)
18+
1319
// Provider implementation for OFREP
1420
type Provider struct {
1521
evaluator Evaluator
@@ -22,6 +28,7 @@ type Option func(*outbound.Configuration)
2228
func NewProvider(baseUri string, options ...Option) *Provider {
2329
cfg := outbound.Configuration{
2430
BaseURI: baseUri,
31+
Timeout: 10 * time.Second,
2532
}
2633

2734
for _, option := range options {
@@ -67,14 +74,21 @@ func (p Provider) Hooks() []openfeature.Hook {
6774

6875
// options of the OFREP provider
6976

70-
// WithHeaderProvider allows to configure a custom header callback to set a custom authorization header
77+
// WithHeader allows to set a custom header to be used for authorization.
78+
func WithHeader(name, value string) func(*outbound.Configuration) {
79+
return WithHeaderProvider(func() (string, string) {
80+
return name, value
81+
})
82+
}
83+
84+
// WithHeaderProvider allows to configure a custom header callback to set a custom authorization header.
7185
func WithHeaderProvider(callback outbound.HeaderCallback) func(*outbound.Configuration) {
7286
return func(c *outbound.Configuration) {
7387
c.Callbacks = append(c.Callbacks, callback)
7488
}
7589
}
7690

77-
// WithBearerToken allows to set token to be used for bearer token authorization
91+
// WithBearerToken allows to set token to be used for bearer token authorization.
7892
func WithBearerToken(token string) func(*outbound.Configuration) {
7993
return func(c *outbound.Configuration) {
8094
c.Callbacks = append(c.Callbacks, func() (string, string) {
@@ -83,7 +97,7 @@ func WithBearerToken(token string) func(*outbound.Configuration) {
8397
}
8498
}
8599

86-
// WithApiKeyAuth allows to set token to be used for api key authorization
100+
// WithApiKeyAuth allows to set token to be used for api key authorization.
87101
func WithApiKeyAuth(token string) func(*outbound.Configuration) {
88102
return func(c *outbound.Configuration) {
89103
c.Callbacks = append(c.Callbacks, func() (string, string) {
@@ -92,9 +106,73 @@ func WithApiKeyAuth(token string) func(*outbound.Configuration) {
92106
}
93107
}
94108

95-
// WithClient allows to provide a pre-configured http.Client for the communication with the OFREP service
109+
// WithClient allows to provide a pre-configured http.Client for the communication with the OFREP service.
96110
func WithClient(client *http.Client) func(configuration *outbound.Configuration) {
97111
return func(configuration *outbound.Configuration) {
98112
configuration.Client = client
99113
}
100114
}
115+
116+
// WithBaseURI allows to set the base URI of the OFREP service.
117+
func WithBaseURI(baseURI string) func(configuration *outbound.Configuration) {
118+
return func(configuration *outbound.Configuration) {
119+
configuration.BaseURI = baseURI
120+
}
121+
}
122+
123+
// WithTimeout allows to set the timeout for the http client used for communication with the OFREP service.
124+
func WithTimeout(timeout time.Duration) func(configuration *outbound.Configuration) {
125+
return func(configuration *outbound.Configuration) {
126+
configuration.Timeout = timeout
127+
}
128+
}
129+
130+
// WithFromEnv uses environment variables to configure the provider.
131+
//
132+
// Experimental: This feature is experimental and may change in future versions.
133+
//
134+
// Supported environment variables:
135+
// - OFREP_ENDPOINT: base URI for the OFREP service
136+
// - OFREP_TIMEOUT: timeout duration (e.g., "30s", "500ms")
137+
// - OFREP_API_KEY: API key for X-API-Key authentication
138+
// - OFREP_BEARER_TOKEN: token for Bearer authentication
139+
// - OFREP_HEADERS: comma-separated custom headers (e.g., "Key1=Value1,Key2=Value2")
140+
func WithFromEnv() func(*outbound.Configuration) {
141+
envHandlers := map[string]func(*outbound.Configuration, string){
142+
"OFREP_ENDPOINT": func(c *outbound.Configuration, v string) {
143+
WithBaseURI(v)(c)
144+
},
145+
"OFREP_TIMEOUT": func(c *outbound.Configuration, v string) {
146+
if t, err := time.ParseDuration(v); err == nil && t > 0 {
147+
WithTimeout(t)(c)
148+
return
149+
}
150+
// as the specification is not finalized, also support raw milliseconds
151+
t, err := strconv.Atoi(v)
152+
if err == nil && t > 0 {
153+
WithTimeout(time.Duration(t) * time.Millisecond)(c)
154+
}
155+
},
156+
"OFREP_API_KEY": func(c *outbound.Configuration, v string) {
157+
WithApiKeyAuth(v)(c)
158+
},
159+
"OFREP_BEARER_TOKEN": func(c *outbound.Configuration, v string) {
160+
WithBearerToken(v)(c)
161+
},
162+
"OFREP_HEADERS": func(c *outbound.Configuration, v string) {
163+
for pair := range strings.SplitSeq(v, ",") {
164+
kv := strings.SplitN(strings.TrimSpace(pair), "=", 2)
165+
if len(kv) == 2 {
166+
WithHeader(kv[0], kv[1])(c)
167+
}
168+
}
169+
},
170+
}
171+
return func(c *outbound.Configuration) {
172+
for key, handler := range envHandlers {
173+
if v := os.Getenv(key); v != "" {
174+
handler(c, v)
175+
}
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)