Skip to content

Commit 7ca092e

Browse files
authored
feat: add context-value flag (#1448)
- add the `--context-value` command line flag to pass arbitrary key value pairs to the evaluation context Signed-off-by: Aleksei Muratov <[email protected]>
1 parent f7dd1eb commit 7ca092e

File tree

16 files changed

+181
-54
lines changed

16 files changed

+181
-54
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ uninstall:
6868
lint:
6969
go install -v github.com/golangci/golangci-lint/cmd/[email protected]
7070
$(foreach module, $(ALL_GO_MOD_DIRS), ${GOPATH}/bin/golangci-lint run --deadline=5m --timeout=5m $(module)/... || exit;)
71+
lint-fix:
72+
go install -v github.com/golangci/golangci-lint/cmd/[email protected]
73+
$(foreach module, $(ALL_GO_MOD_DIRS), ${GOPATH}/bin/golangci-lint run --fix --deadline=5m --timeout=5m $(module)/... || exit;)
7174
install-mockgen:
7275
go install go.uber.org/mock/[email protected]
7376
mockgen: install-mockgen

core/pkg/service/iservice.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Configuration struct {
3232
SocketPath string
3333
CORS []string
3434
Options []connect.HandlerOption
35+
ContextValues map[string]any
3536
}
3637

3738
/*

docs/reference/flag-definitions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ For example, when accessing flagd via HTTP, the POST body may look like this:
184184

185185
The evaluation context can be accessed in targeting rules using the `var` operation followed by the evaluation context property name.
186186

187+
The evaluation context can be appended by arbitrary key value pairs
188+
via the `-X` command line flag.
189+
187190
| Description | Example |
188191
| -------------------------------------------------------------- | ---------------------------------------------------- |
189192
| Retrieve property from the evaluation context | `#!json { "var": "email" }` |

docs/reference/flagd-cli/flagd_start.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ flagd start [flags]
1111
### Options
1212

1313
```
14+
-X, --context-value stringToString add arbitrary key value pairs to the flag evaluation context (default [])
1415
-C, --cors-origin strings CORS allowed origins, * will allow all origins
1516
-h, --help help for start
1617
-z, --log-format string Set the logging format, e.g. console or json (default "console")

flagd/cmd/start.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ const (
3434
sourcesFlagName = "sources"
3535
syncPortFlagName = "sync-port"
3636
uriFlagName = "uri"
37+
contextValueFlagName = "context-value"
3738
)
3839

3940
func init() {
4041
flags := startCmd.Flags()
41-
4242
// allows environment variables to use _ instead of -
4343
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) // sync-provider-args becomes SYNC_PROVIDER_ARGS
4444
viper.SetEnvPrefix("FLAGD") // port becomes FLAGD_PORT
@@ -78,6 +78,8 @@ func init() {
7878
flags.StringP(otelCAPathFlagName, "A", "", "tls certificate authority path to use with OpenTelemetry collector")
7979
flags.DurationP(otelReloadIntervalFlagName, "I", time.Hour, "how long between reloading the otel tls certificate "+
8080
"from disk")
81+
flags.StringToStringP(contextValueFlagName, "X", map[string]string{}, "add arbitrary key value pairs "+
82+
"to the flag evaluation context")
8183

8284
_ = viper.BindPFlag(corsFlagName, flags.Lookup(corsFlagName))
8385
_ = viper.BindPFlag(logFormatFlagName, flags.Lookup(logFormatFlagName))
@@ -95,6 +97,7 @@ func init() {
9597
_ = viper.BindPFlag(uriFlagName, flags.Lookup(uriFlagName))
9698
_ = viper.BindPFlag(syncPortFlagName, flags.Lookup(syncPortFlagName))
9799
_ = viper.BindPFlag(ofrepPortFlagName, flags.Lookup(ofrepPortFlagName))
100+
_ = viper.BindPFlag(contextValueFlagName, flags.Lookup(contextValueFlagName))
98101
}
99102

100103
// startCmd represents the start command
@@ -139,6 +142,11 @@ var startCmd = &cobra.Command{
139142
}
140143
syncProviders = append(syncProviders, syncProvidersFromConfig...)
141144

145+
contextValuesToMap := make(map[string]any)
146+
for k, v := range viper.GetStringMapString(contextValueFlagName) {
147+
contextValuesToMap[k] = v
148+
}
149+
142150
// Build Runtime -----------------------------------------------------------
143151
rt, err := runtime.FromConfig(logger, Version, runtime.Config{
144152
CORS: viper.GetStringSlice(corsFlagName),
@@ -156,6 +164,7 @@ var startCmd = &cobra.Command{
156164
ServiceSocketPath: viper.GetString(socketPathFlagName),
157165
SyncServicePort: viper.GetUint16(syncPortFlagName),
158166
SyncProviders: syncProviders,
167+
ContextValues: contextValuesToMap,
159168
})
160169
if err != nil {
161170
rtLogger.Fatal(err.Error())

flagd/pkg/runtime/from_config.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ type Config struct {
4040

4141
SyncProviders []sync.SourceConfig
4242
CORS []string
43+
44+
ContextValues map[string]any
4345
}
4446

4547
// FromConfig builds a runtime from startup configurations
@@ -101,17 +103,20 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
101103
ofrepService, err := ofrep.NewOfrepService(jsonEvaluator, config.CORS, ofrep.SvcConfiguration{
102104
Logger: logger.WithFields(zap.String("component", "OFREPService")),
103105
Port: config.OfrepServicePort,
104-
})
106+
},
107+
config.ContextValues,
108+
)
105109
if err != nil {
106110
return nil, fmt.Errorf("error creating ofrep service")
107111
}
108112

109113
// flag sync service
110114
flagSyncService, err := flagsync.NewSyncService(flagsync.SvcConfigurations{
111-
Logger: logger.WithFields(zap.String("component", "FlagSyncService")),
112-
Port: config.SyncServicePort,
113-
Sources: sources,
114-
Store: s,
115+
Logger: logger.WithFields(zap.String("component", "FlagSyncService")),
116+
Port: config.SyncServicePort,
117+
Sources: sources,
118+
Store: s,
119+
ContextValues: config.ContextValues,
115120
})
116121
if err != nil {
117122
return nil, fmt.Errorf("error creating sync service: %w", err)
@@ -145,6 +150,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
145150
SocketPath: config.ServiceSocketPath,
146151
CORS: config.CORS,
147152
Options: options,
153+
ContextValues: config.ContextValues,
148154
},
149155
SyncImpl: iSyncs,
150156
}, nil

flagd/pkg/service/flag-evaluation/connect_service.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listene
154154
s.eval,
155155
s.eventingConfiguration,
156156
s.metrics,
157+
svcConf.ContextValues,
157158
)
158159

159160
marshalOpts := WithJSON(
@@ -170,6 +171,7 @@ func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listene
170171
s.eval,
171172
s.eventingConfiguration,
172173
s.metrics,
174+
svcConf.ContextValues,
173175
)
174176

175177
_, newHandler := evaluationV1.NewServiceHandler(newFes, append(svcConf.Options, marshalOpts)...)

flagd/pkg/service/flag-evaluation/flag_evaluator.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,24 @@ type OldFlagEvaluationService struct {
3232
metrics telemetry.IMetricsRecorder
3333
eventingConfiguration IEvents
3434
flagEvalTracer trace.Tracer
35+
contextValues map[string]any
3536
}
3637

3738
// NewOldFlagEvaluationService creates a OldFlagEvaluationService with provided parameters
38-
func NewOldFlagEvaluationService(log *logger.Logger,
39-
eval evaluator.IEvaluator, eventingCfg IEvents, metricsRecorder telemetry.IMetricsRecorder,
39+
func NewOldFlagEvaluationService(
40+
log *logger.Logger,
41+
eval evaluator.IEvaluator,
42+
eventingCfg IEvents,
43+
metricsRecorder telemetry.IMetricsRecorder,
44+
contextValues map[string]any,
4045
) *OldFlagEvaluationService {
4146
svc := &OldFlagEvaluationService{
4247
logger: log,
4348
eval: eval,
4449
metrics: &telemetry.NoopMetricsRecorder{},
4550
eventingConfiguration: eventingCfg,
4651
flagEvalTracer: otel.Tracer("flagEvaluationService"),
52+
contextValues: contextValues,
4753
}
4854

4955
if metricsRecorder != nil {
@@ -65,12 +71,8 @@ func (s *OldFlagEvaluationService) ResolveAll(
6571
res := &schemaV1.ResolveAllResponse{
6672
Flags: make(map[string]*schemaV1.AnyFlag),
6773
}
68-
evalCtx := map[string]any{}
69-
if e := req.Msg.GetContext(); e != nil {
70-
evalCtx = e.AsMap()
71-
}
7274

73-
values, err := s.eval.ResolveAllValues(sCtx, reqID, evalCtx)
75+
values, err := s.eval.ResolveAllValues(sCtx, reqID, mergeContexts(req.Msg.GetContext().AsMap(), s.contextValues))
7476
if err != nil {
7577
s.logger.WarnWithID(reqID, fmt.Sprintf("error resolving all flags: %v", err))
7678
return nil, fmt.Errorf("error resolving flags. Tracking ID: %s", reqID)
@@ -172,6 +174,7 @@ func (s *OldFlagEvaluationService) ResolveBoolean(
172174
sCtx, span := s.flagEvalTracer.Start(ctx, "resolveBoolean", trace.WithSpanKind(trace.SpanKindServer))
173175
defer span.End()
174176
res := connect.NewResponse(&schemaV1.ResolveBooleanResponse{})
177+
175178
err := resolve[bool](
176179
sCtx,
177180
s.logger,
@@ -180,6 +183,7 @@ func (s *OldFlagEvaluationService) ResolveBoolean(
180183
req.Msg.GetContext(),
181184
&booleanResponse{schemaV1Resp: res},
182185
s.metrics,
186+
s.contextValues,
183187
)
184188
if err != nil {
185189
span.RecordError(err)
@@ -206,6 +210,7 @@ func (s *OldFlagEvaluationService) ResolveString(
206210
req.Msg.GetContext(),
207211
&stringResponse{schemaV1Resp: res},
208212
s.metrics,
213+
s.contextValues,
209214
)
210215
if err != nil {
211216
span.RecordError(err)
@@ -232,6 +237,7 @@ func (s *OldFlagEvaluationService) ResolveInt(
232237
req.Msg.GetContext(),
233238
&intResponse{schemaV1Resp: res},
234239
s.metrics,
240+
s.contextValues,
235241
)
236242
if err != nil {
237243
span.RecordError(err)
@@ -258,6 +264,7 @@ func (s *OldFlagEvaluationService) ResolveFloat(
258264
req.Msg.GetContext(),
259265
&floatResponse{schemaV1Resp: res},
260266
s.metrics,
267+
s.contextValues,
261268
)
262269
if err != nil {
263270
span.RecordError(err)
@@ -284,6 +291,7 @@ func (s *OldFlagEvaluationService) ResolveObject(
284291
req.Msg.GetContext(),
285292
&objectResponse{schemaV1Resp: res},
286293
s.metrics,
294+
s.contextValues,
287295
)
288296
if err != nil {
289297
span.RecordError(err)
@@ -293,21 +301,36 @@ func (s *OldFlagEvaluationService) ResolveObject(
293301
return res, err
294302
}
295303

304+
// mergeContexts combines values from the request context with the values from the config --context-values flag.
305+
// Request context values have a higher priority.
306+
func mergeContexts(reqCtx, configFlagsCtx map[string]any) map[string]any {
307+
merged := make(map[string]any)
308+
for k, v := range reqCtx {
309+
merged[k] = v
310+
}
311+
for k, v := range configFlagsCtx {
312+
merged[k] = v
313+
}
314+
return merged
315+
}
316+
296317
// resolve is a generic flag resolver
297318
func resolve[T constraints](ctx context.Context, logger *logger.Logger, resolver resolverSignature[T], flagKey string,
298319
evaluationContext *structpb.Struct, resp response[T], metrics telemetry.IMetricsRecorder,
320+
configContextValues map[string]any,
299321
) error {
300322
reqID := xid.New().String()
301323
defer logger.ClearFields(reqID)
302324

325+
mergedContext := mergeContexts(evaluationContext.AsMap(), configContextValues)
303326
logger.WriteFields(
304327
reqID,
305328
zap.String("flag-key", flagKey),
306-
zap.Strings("context-keys", formatContextKeys(evaluationContext)),
329+
zap.Strings("context-keys", formatContextKeys(mergedContext)),
307330
)
308331

309332
var evalErrFormatted error
310-
result, variant, reason, metadata, evalErr := resolver(ctx, reqID, flagKey, evaluationContext.AsMap())
333+
result, variant, reason, metadata, evalErr := resolver(ctx, reqID, flagKey, mergedContext)
311334
if evalErr != nil {
312335
logger.WarnWithID(reqID, fmt.Sprintf("returning error response, reason: %v", evalErr))
313336
reason = model.ErrorReason
@@ -329,9 +352,9 @@ func resolve[T constraints](ctx context.Context, logger *logger.Logger, resolver
329352
return evalErrFormatted
330353
}
331354

332-
func formatContextKeys(context *structpb.Struct) []string {
355+
func formatContextKeys(context map[string]any) []string {
333356
res := []string{}
334-
for k := range context.AsMap() {
357+
for k := range context {
335358
res = append(res, k)
336359
}
337360
return res

flagd/pkg/service/flag-evaluation/flag_evaluator_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ func TestConnectService_ResolveAll(t *testing.T) {
128128
eval,
129129
&eventingConfiguration{},
130130
metrics,
131+
nil,
131132
)
132133
got, err := s.ResolveAll(context.Background(), connect.NewRequest(tt.req))
133134
if err != nil && !errors.Is(err, tt.wantErr) {
@@ -235,6 +236,7 @@ func TestFlag_Evaluation_ResolveBoolean(t *testing.T) {
235236
eval,
236237
&eventingConfiguration{},
237238
metrics,
239+
nil,
238240
)
239241
got, err := s.ResolveBoolean(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
240242
if (err != nil) && !errors.Is(err, tt.wantErr) {
@@ -290,6 +292,7 @@ func BenchmarkFlag_Evaluation_ResolveBoolean(b *testing.B) {
290292
eval,
291293
&eventingConfiguration{},
292294
metrics,
295+
nil,
293296
)
294297
b.Run(name, func(b *testing.B) {
295298
for i := 0; i < b.N; i++ {
@@ -388,6 +391,7 @@ func TestFlag_Evaluation_ResolveString(t *testing.T) {
388391
eval,
389392
&eventingConfiguration{},
390393
metrics,
394+
nil,
391395
)
392396
got, err := s.ResolveString(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
393397
if (err != nil) && !errors.Is(err, tt.wantErr) {
@@ -443,6 +447,7 @@ func BenchmarkFlag_Evaluation_ResolveString(b *testing.B) {
443447
eval,
444448
&eventingConfiguration{},
445449
metrics,
450+
nil,
446451
)
447452
b.Run(name, func(b *testing.B) {
448453
for i := 0; i < b.N; i++ {
@@ -540,6 +545,7 @@ func TestFlag_Evaluation_ResolveFloat(t *testing.T) {
540545
eval,
541546
&eventingConfiguration{},
542547
metrics,
548+
nil,
543549
)
544550
got, err := s.ResolveFloat(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
545551
if (err != nil) && !errors.Is(err, tt.wantErr) {
@@ -595,6 +601,7 @@ func BenchmarkFlag_Evaluation_ResolveFloat(b *testing.B) {
595601
eval,
596602
&eventingConfiguration{},
597603
metrics,
604+
nil,
598605
)
599606
b.Run(name, func(b *testing.B) {
600607
for i := 0; i < b.N; i++ {
@@ -692,6 +699,7 @@ func TestFlag_Evaluation_ResolveInt(t *testing.T) {
692699
eval,
693700
&eventingConfiguration{},
694701
metrics,
702+
nil,
695703
)
696704
got, err := s.ResolveInt(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
697705
if (err != nil) && !errors.Is(err, tt.wantErr) {
@@ -747,6 +755,7 @@ func BenchmarkFlag_Evaluation_ResolveInt(b *testing.B) {
747755
eval,
748756
&eventingConfiguration{},
749757
metrics,
758+
nil,
750759
)
751760
b.Run(name, func(b *testing.B) {
752761
for i := 0; i < b.N; i++ {
@@ -847,6 +856,7 @@ func TestFlag_Evaluation_ResolveObject(t *testing.T) {
847856
eval,
848857
&eventingConfiguration{},
849858
metrics,
859+
nil,
850860
)
851861

852862
outParsed, err := structpb.NewStruct(tt.evalFields.result)
@@ -910,6 +920,7 @@ func BenchmarkFlag_Evaluation_ResolveObject(b *testing.B) {
910920
eval,
911921
&eventingConfiguration{},
912922
metrics,
923+
nil,
913924
)
914925
if name != "eval returns error" {
915926
outParsed, err := structpb.NewStruct(tt.evalFields.result)

0 commit comments

Comments
 (0)