Skip to content

Commit 3ac923c

Browse files
feat(flagd)!: add file mode to flagd provider (#648)
Signed-off-by: Raphael Wigoutschnigg <[email protected]>
1 parent 1a940be commit 3ac923c

File tree

6 files changed

+245
-53
lines changed

6 files changed

+245
-53
lines changed

providers/flagd/README.md

+36-34
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ Flag evaluations take place remotely at the connected flagd instance.
2222
To use in this mode, set the provider to the `openfeature` global singleton as shown below (using default values which align with those of `flagd`)
2323

2424
```go
25-
openfeature.SetProvider(flagd.NewProvider())
25+
provider, err := flagd.NewProvider()
26+
openfeature.SetProvider(provider)
2627
```
2728

2829
### In-process resolver
@@ -33,30 +34,12 @@ Flag configurations for evaluation are obtained via gRPC protocol using [sync pr
3334
Consider following example to create a `FlagdProvider` with in-process evaluations,
3435

3536
```go
36-
provider := flagd.NewProvider(
37-
flagd.WithInProcessResolver(),
38-
flagd.WithHost("localhost"),
39-
flagd.WithPort(8013))
37+
provider, err := flagd.NewProvider(flagd.WithInProcessResolver())
4038
openfeature.SetProvider(provider)
4139
```
4240

4341
In the above example, in-process handlers attempt to connect to a sync service on address `localhost:8013` to obtain [flag definitions](https://github.com/open-feature/schemas/blob/main/json/flagd-definitions.json).
4442

45-
#### Offline mode
46-
47-
In-process resolvers can also work in an offline mode.
48-
To enable this mode, you should provide a [valid flag configuration](https://flagd.dev/reference/flag-definitions/) file with the option `WithOfflineFilePath`.
49-
50-
```go
51-
provider := flagd.NewProvider(
52-
flagd.WithInProcessResolver(),
53-
flagd.WithOfflineFilePath(OFFLINE_FLAG_PATH))
54-
openfeature.SetProvider(provider)
55-
```
56-
57-
The provider will attempt to detect file changes, but this is a best-effort attempt as file system events differ between operating systems.
58-
This mode is useful for local development, tests and offline applications.
59-
6043
#### Custom sync provider
6144

6245
In-process resolver can also be configured with a custom sync provider to change how the in-process resolver fetches flags.
@@ -65,28 +48,44 @@ The custom sync provider must implement the [sync.ISync interface](https://githu
6548
```go
6649
var syncProvider sync.ISync = MyAwesomeSyncProvider{}
6750

68-
provider := flagd.NewProvider(
51+
provider, err := flagd.NewProvider(
6952
flagd.WithInProcessResolver(),
70-
flagd.WithCustomSyncProvider(syncProvider))
53+
flagd.WithCustomSyncProvider(syncProvider),
54+
)
7155
openfeature.SetProvider(provider)
7256
```
7357

7458
```go
7559
var syncProvider sync.ISync = MyAwesomeSyncProvider{}
7660
var syncProviderUri string = "myawesome://sync.uri"
7761

78-
provider := flagd.NewProvider(
62+
provider, err := flagd.NewProvider(
7963
flagd.WithInProcessResolver(),
80-
flagd.WithCustomSyncProviderAndUri(syncProvider, syncProviderUri))
64+
flagd.WithCustomSyncProviderAndUri(syncProvider, syncProviderUri),
65+
)
8166
openfeature.SetProvider(provider)
8267
```
8368

8469
> [!IMPORTANT]
8570
> Note that the in-process resolver can only use a single flag source.
8671
> If multiple sources are configured then only one would be selected based on the following order of preference:
8772
> 1. Custom sync provider
88-
> 2. Offline file
89-
> 3. gRPC
73+
> 2. gRPC
74+
75+
### File mode
76+
77+
This mode obtains the flag configurations from a local file and performs flag evaluations locally.
78+
79+
```go
80+
provider, err := flagd.NewProvider(
81+
flagd.WithFileResolver(),
82+
flagd.WithOfflineFilePath(OFFLINE_FLAG_PATH),
83+
)
84+
openfeature.SetProvider(provider)
85+
```
86+
87+
The provider will attempt to detect file changes, but this is a best-effort attempt as file system events differ between operating systems.
88+
This mode is useful for local development, tests and offline applications.
9089

9190
## Configuration options
9291

@@ -102,7 +101,7 @@ Configuration can be provided as constructor options or as environment variables
102101
| WithCertificatePath | FLAGD_SERVER_CERT_PATH | string | "" | rpc & in-process |
103102
| WithLRUCache<br/>WithBasicInMemoryCache<br/>WithoutCache | FLAGD_CACHE | string (lru, mem, disabled) | lru | rpc |
104103
| WithEventStreamConnectionMaxAttempts | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc |
105-
| WithOfflineFilePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | "" | in-process |
104+
| WithOfflineFilePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | "" | file |
106105
| WithProviderID | FLAGD_SOURCE_PROVIDER_ID | string | "" | in-process |
107106
| WithSelector | FLAGD_SOURCE_SELECTOR | string | "" | in-process |
108107

@@ -115,10 +114,11 @@ In the event that another configuration option is passed to the `flagd.NewProvid
115114

116115
e.g. below the values set by `FromEnv()` overwrite the value set by `WithHost("localhost")`.
117116
```go
118-
openfeature.SetProvider(flagd.NewProvider(
117+
provider, err := flagd.NewProvider(
119118
flagd.WithHost("localhost"),
120119
flagd.FromEnv(),
121-
))
120+
)
121+
openfeature.SetProvider(provider)
122122
```
123123

124124
### Caching
@@ -138,10 +138,11 @@ and one custom resolver for `envoy` proxy resolution. For more details, please r
138138
[RFC](https://github.com/open-feature/flagd/blob/main/docs/reference/specifications/proposal/rfc-grpc-custom-name-resolver.md) document.
139139

140140
```go
141-
openfeature.SetProvider(flagd.NewProvider(
141+
provider, err := flagd.NewProvider(
142142
flagd.WithInProcessResolver(),
143143
flagd.WithTargetUri("envoy://localhost:9211/test.service"),
144-
))
144+
)
145+
openfeature.SetProvider(provider)
145146
```
146147

147148
### gRPC DialOptions override
@@ -158,11 +159,12 @@ dialOptions := []grpc.DialOption{
158159
grpc.WithAuthority(...),
159160
}
160161

161-
openfeature.SetProvider(flagd.NewProvider(
162+
provider, err := flagd.NewProvider(
162163
flagd.WithInProcessResolver(),
163164
flagd.WithHost("example.com/flagdSyncApi"), flagd.WithPort(443),
164165
flagd.WithGrpcDialOptionsOverride(dialOptions),
165-
))
166+
)
167+
openfeature.SetProvider(provider)
166168
```
167169

168170
## Supported Events
@@ -199,7 +201,7 @@ for many of the popular logger packages.
199201
var l logr.Logger
200202
l = integratedlogr.New() // replace with your chosen integrator
201203

202-
provider := flagd.NewProvider(flagd.WithLogger(l)) // set the provider's logger
204+
provider, err := flagd.NewProvider(flagd.WithLogger(l)) // set the provider's logger
203205
```
204206

205207
[logr](https://github.com/go-logr/logr) uses incremental verbosity levels (akin to named levels but in integer form).

providers/flagd/e2e/evaluation_test.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ func TestETestEvaluationFlagdInRPC(t *testing.T) {
2525
testSuite := godog.TestSuite{
2626
Name: name,
2727
TestSuiteInitializer: integration.InitializeTestSuite(func() openfeature.FeatureProvider {
28-
return flagd.NewProvider(flagd.WithPort(8013))
28+
provider, err := flagd.NewProvider(flagd.WithPort(8013))
29+
30+
if err != nil {
31+
t.Fatal("Creating provider failed:", err)
32+
}
33+
34+
return provider
2935
}),
3036
ScenarioInitializer: integration.InitializeEvaluationScenario,
3137
Options: &godog.Options{
@@ -54,7 +60,13 @@ func TestJsonEvaluatorFlagdInProcess(t *testing.T) {
5460
testSuite := godog.TestSuite{
5561
Name: name,
5662
TestSuiteInitializer: integration.InitializeTestSuite(func() openfeature.FeatureProvider {
57-
return flagd.NewProvider(flagd.WithInProcessResolver(), flagd.WithPort(9090))
63+
provider, err := flagd.NewProvider(flagd.WithInProcessResolver(), flagd.WithPort(9090))
64+
65+
if err != nil {
66+
t.Fatal("Creating provider failed:", err)
67+
}
68+
69+
return provider
5870
}),
5971
ScenarioInitializer: integration.InitializeEvaluationScenario,
6072
Options: &godog.Options{

providers/flagd/e2e/json_evalutor_test.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ func TestJsonEvaluatorInRPC(t *testing.T) {
2525
testSuite := godog.TestSuite{
2626
Name: name,
2727
TestSuiteInitializer: integration.InitializeFlagdJsonTestSuite(func() openfeature.FeatureProvider {
28-
return flagd.NewProvider(flagd.WithPort(8013))
28+
provider, err := flagd.NewProvider(flagd.WithPort(8013))
29+
30+
if err != nil {
31+
t.Fatal("Creating provider failed:", err)
32+
}
33+
34+
return provider
2935
}),
3036
ScenarioInitializer: integration.InitializeFlagdJsonScenario,
3137
Options: &godog.Options{
@@ -54,7 +60,13 @@ func TestJsonEvaluatorInProcess(t *testing.T) {
5460
testSuite := godog.TestSuite{
5561
Name: name,
5662
TestSuiteInitializer: integration.InitializeFlagdJsonTestSuite(func() openfeature.FeatureProvider {
57-
return flagd.NewProvider(flagd.WithInProcessResolver(), flagd.WithPort(9090))
63+
provider, err := flagd.NewProvider(flagd.WithInProcessResolver(), flagd.WithPort(9090))
64+
65+
if err != nil {
66+
t.Fatal("Creating provider failed:", err)
67+
}
68+
69+
return provider
5870
}),
5971
ScenarioInitializer: integration.InitializeFlagdJsonScenario,
6072
Options: &godog.Options{

providers/flagd/pkg/configuration.go

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626

2727
rpc ResolverType = "rpc"
2828
inProcess ResolverType = "in-process"
29+
file ResolverType = "file"
2930

3031
flagdHostEnvironmentVariableName = "FLAGD_HOST"
3132
flagdPortEnvironmentVariableName = "FLAGD_PORT"
@@ -155,6 +156,8 @@ func (cfg *providerConfiguration) updateFromEnvVar() {
155156
cfg.Resolver = rpc
156157
case inProcess:
157158
cfg.Resolver = inProcess
159+
case file:
160+
cfg.Resolver = file
158161
default:
159162
cfg.log.Info("invalid resolver type: %s, falling back to default: %s", resolver, defaultResolver)
160163
cfg.Resolver = defaultResolver

providers/flagd/pkg/provider.go

+46-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package flagd
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
parallel "sync"
@@ -31,7 +32,7 @@ type Provider struct {
3132
eventStream chan of.Event
3233
}
3334

34-
func NewProvider(opts ...ProviderOption) *Provider {
35+
func NewProvider(opts ...ProviderOption) (*Provider, error) {
3536
log := logr.New(logger.Logger{})
3637

3738
// initialize with default configurations
@@ -50,12 +51,12 @@ func NewProvider(opts ...ProviderOption) *Provider {
5051
opt(provider)
5152
}
5253

53-
if provider.providerConfiguration.Port == 0 {
54-
if provider.providerConfiguration.Resolver == inProcess {
55-
provider.providerConfiguration.Port = defaultInProcessPort
56-
} else {
57-
provider.providerConfiguration.Port = defaultRpcPort
58-
}
54+
configureProvider(provider)
55+
56+
err := validateProvider(provider)
57+
58+
if err != nil {
59+
return nil, err
5960
}
6061

6162
cacheService := cache.NewCacheService(
@@ -77,7 +78,7 @@ func NewProvider(opts ...ProviderOption) *Provider {
7778
cacheService,
7879
provider.logger,
7980
provider.providerConfiguration.EventStreamConnectionMaxAttempts)
80-
} else {
81+
} else if provider.providerConfiguration.Resolver == inProcess {
8182
service = process.NewInProcessService(process.Configuration{
8283
Host: provider.providerConfiguration.Host,
8384
Port: provider.providerConfiguration.Port,
@@ -90,11 +91,39 @@ func NewProvider(opts ...ProviderOption) *Provider {
9091
CustomSyncProviderUri: provider.providerConfiguration.CustomSyncProviderUri,
9192
GrpcDialOptionsOverride: provider.providerConfiguration.GrpcDialOptionsOverride,
9293
})
94+
} else {
95+
service = process.NewInProcessService(process.Configuration{
96+
OfflineFlagSource: provider.providerConfiguration.OfflineFlagSourcePath,
97+
})
9398
}
9499

95100
provider.service = service
96101

97-
return provider
102+
return provider, nil
103+
}
104+
105+
func configureProvider(p *Provider) {
106+
if len(p.providerConfiguration.OfflineFlagSourcePath) > 0 && p.providerConfiguration.Resolver == inProcess {
107+
p.providerConfiguration.Resolver = file
108+
}
109+
110+
if p.providerConfiguration.Port == 0 {
111+
switch p.providerConfiguration.Resolver {
112+
case rpc:
113+
p.providerConfiguration.Port = defaultRpcPort
114+
case inProcess:
115+
p.providerConfiguration.Port = defaultInProcessPort
116+
}
117+
}
118+
}
119+
120+
func validateProvider(p *Provider) error {
121+
// We need a file path for file mode
122+
if len(p.providerConfiguration.OfflineFlagSourcePath) == 0 && p.providerConfiguration.Resolver == file {
123+
return errors.New("Resolver Type 'file' requires a OfflineFlagSourcePath")
124+
}
125+
126+
return nil
98127
}
99128

100129
func (p *Provider) Init(_ of.EvaluationContext) error {
@@ -315,14 +344,20 @@ func WithInProcessResolver() ProviderOption {
315344
}
316345
}
317346

318-
// WithOfflineFilePath file path to obtain flags to run provider in offline mode with in-process evaluations.
319-
// This is only useful with inProcess resolver type
347+
// WithOfflineFilePath file path to obtain flags used for provider in file mode.
320348
func WithOfflineFilePath(path string) ProviderOption {
321349
return func(p *Provider) {
322350
p.providerConfiguration.OfflineFlagSourcePath = path
323351
}
324352
}
325353

354+
// WithFileResolver sets flag resolver to File
355+
func WithFileResolver() ProviderOption {
356+
return func(p *Provider) {
357+
p.providerConfiguration.Resolver = file
358+
}
359+
}
360+
326361
// WithSelector sets the selector to be used for InProcess flag sync calls
327362
func WithSelector(selector string) ProviderOption {
328363
return func(p *Provider) {

0 commit comments

Comments
 (0)