Skip to content

Commit ba34815

Browse files
feat: updating context using headers (#1641)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> Able to update context map using headers present in - OFREP requests - Connect Requests (via Flag Evaluator V2 service) ### Related Issues Fixes #1583 ### Notes Context values passed via headers is high priority If same context key is updated via - Headers - Request Body - Static Config _Context via Headers will be considered_ ### Usage ``` flagd start --port 8013 --uri file:./samples/example_flags.flagd.json -H Header=contextKey ``` or ``` flagd start --port 8013 --uri file:./samples/example_flags.flagd.json --context-from-header Header=contextKey ``` --------- Signed-off-by: Rahul Baradol <rahul.baradol.14@gmail.com>
1 parent bf10ff3 commit ba34815

File tree

11 files changed

+206
-98
lines changed

11 files changed

+206
-98
lines changed

core/pkg/service/iservice.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,17 @@ type Notification struct {
2323
type ReadinessProbe func() bool
2424

2525
type Configuration struct {
26-
ReadinessProbe ReadinessProbe
27-
Port uint16
28-
ManagementPort uint16
29-
ServiceName string
30-
CertPath string
31-
KeyPath string
32-
SocketPath string
33-
CORS []string
34-
Options []connect.HandlerOption
35-
ContextValues map[string]any
26+
ReadinessProbe ReadinessProbe
27+
Port uint16
28+
ManagementPort uint16
29+
ServiceName string
30+
CertPath string
31+
KeyPath string
32+
SocketPath string
33+
CORS []string
34+
Options []connect.HandlerOption
35+
ContextValues map[string]any
36+
HeaderToContextKeyMappings map[string]string
3637
}
3738

3839
/*

docs/reference/flagd-cli/flagd_start.md

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,27 @@ flagd start [flags]
1111
### Options
1212

1313
```
14-
-X, --context-value stringToString add arbitrary key value pairs to the flag evaluation context (default [])
15-
-C, --cors-origin strings CORS allowed origins, * will allow all origins
16-
-h, --help help for start
17-
-z, --log-format string Set the logging format, e.g. console or json (default "console")
18-
-m, --management-port int32 Port for management operations (default 8014)
19-
-t, --metrics-exporter string Set the metrics exporter. Default(if unset) is Prometheus. Can be override to otel - OpenTelemetry metric exporter. Overriding to otel require otelCollectorURI to be present
20-
-r, --ofrep-port int32 ofrep service port (default 8016)
21-
-A, --otel-ca-path string tls certificate authority path to use with OpenTelemetry collector
22-
-D, --otel-cert-path string tls certificate path to use with OpenTelemetry collector
23-
-o, --otel-collector-uri string Set the grpc URI of the OpenTelemetry collector for flagd runtime. If unset, the collector setup will be ignored and traces will not be exported.
24-
-K, --otel-key-path string tls key path to use with OpenTelemetry collector
25-
-I, --otel-reload-interval duration how long between reloading the otel tls certificate from disk (default 1h0m0s)
26-
-p, --port int32 Port to listen on (default 8013)
27-
-c, --server-cert-path string Server side tls certificate path
28-
-k, --server-key-path string Server side tls key path
29-
-d, --socket-path string Flagd unix socket path. With grpc the evaluations service will become available on this address. With http(s) the grpc-gateway proxy will use this address internally.
30-
-s, --sources string JSON representation of an array of SourceConfig objects. This object contains 2 required fields, uri (string) and provider (string). Documentation for this object: https://flagd.dev/reference/sync-configuration/#source-configuration
31-
-g, --sync-port int32 gRPC Sync port (default 8015)
32-
-e, --sync-socket-path string Flagd sync service socket path. With grpc the sync service will be available on this address.
33-
-f, --uri .yaml/.yml/.json Set a sync provider uri to read data from, this can be a filepath, URL (HTTP and gRPC), FeatureFlag custom resource, or GCS or Azure Blob. When flag keys are duplicated across multiple providers the merge priority follows the index of the flag arguments, as such flags from the uri at index 0 take the lowest precedence, with duplicated keys being overwritten by those from the uri at index 1. Please note that if you are using filepath, flagd only supports files with .yaml/.yml/.json extension.
14+
-H, --context-from-header stringToString add key-value pairs to map header values to context values, where key is Header name, value is context key (default [])
15+
-X, --context-value stringToString add arbitrary key value pairs to the flag evaluation context (default [])
16+
-C, --cors-origin strings CORS allowed origins, * will allow all origins
17+
-h, --help help for start
18+
-z, --log-format string Set the logging format, e.g. console or json (default "console")
19+
-m, --management-port int32 Port for management operations (default 8014)
20+
-t, --metrics-exporter string Set the metrics exporter. Default(if unset) is Prometheus. Can be override to otel - OpenTelemetry metric exporter. Overriding to otel require otelCollectorURI to be present
21+
-r, --ofrep-port int32 ofrep service port (default 8016)
22+
-A, --otel-ca-path string tls certificate authority path to use with OpenTelemetry collector
23+
-D, --otel-cert-path string tls certificate path to use with OpenTelemetry collector
24+
-o, --otel-collector-uri string Set the grpc URI of the OpenTelemetry collector for flagd runtime. If unset, the collector setup will be ignored and traces will not be exported.
25+
-K, --otel-key-path string tls key path to use with OpenTelemetry collector
26+
-I, --otel-reload-interval duration how long between reloading the otel tls certificate from disk (default 1h0m0s)
27+
-p, --port int32 Port to listen on (default 8013)
28+
-c, --server-cert-path string Server side tls certificate path
29+
-k, --server-key-path string Server side tls key path
30+
-d, --socket-path string Flagd unix socket path. With grpc the evaluations service will become available on this address. With http(s) the grpc-gateway proxy will use this address internally.
31+
-s, --sources string JSON representation of an array of SourceConfig objects. This object contains 2 required fields, uri (string) and provider (string). Documentation for this object: https://flagd.dev/reference/sync-configuration/#source-configuration
32+
-g, --sync-port int32 gRPC Sync port (default 8015)
33+
-e, --sync-socket-path string Flagd sync service socket path. With grpc the sync service will be available on this address.
34+
-f, --uri .yaml/.yml/.json Set a sync provider uri to read data from, this can be a filepath, URL (HTTP and gRPC), FeatureFlag custom resource, or GCS or Azure Blob. When flag keys are duplicated across multiple providers the merge priority follows the index of the flag arguments, as such flags from the uri at index 0 take the lowest precedence, with duplicated keys being overwritten by those from the uri at index 1. Please note that if you are using filepath, flagd only supports files with .yaml/.yml/.json extension.
3435
```
3536

3637
### Options inherited from parent commands

flagd/cmd/start.go

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const (
3737
syncSocketPathFlagName = "sync-socket-path"
3838
uriFlagName = "uri"
3939
contextValueFlagName = "context-value"
40+
headerToContextKeyFlagName = "context-from-header"
4041
)
4142

4243
func init() {
@@ -84,6 +85,8 @@ func init() {
8485
"from disk")
8586
flags.StringToStringP(contextValueFlagName, "X", map[string]string{}, "add arbitrary key value pairs "+
8687
"to the flag evaluation context")
88+
flags.StringToStringP(headerToContextKeyFlagName, "H", map[string]string{}, "add key-value pairs to map " +
89+
"header values to context values, where key is Header name, value is context key")
8790

8891
bindFlags(flags)
8992
}
@@ -107,6 +110,7 @@ func bindFlags(flags *pflag.FlagSet) {
107110
_ = viper.BindPFlag(syncSocketPathFlagName, flags.Lookup(syncSocketPathFlagName))
108111
_ = viper.BindPFlag(ofrepPortFlagName, flags.Lookup(ofrepPortFlagName))
109112
_ = viper.BindPFlag(contextValueFlagName, flags.Lookup(contextValueFlagName))
113+
_ = viper.BindPFlag(headerToContextKeyFlagName, flags.Lookup(headerToContextKeyFlagName))
110114
}
111115

112116
// startCmd represents the start command
@@ -156,25 +160,31 @@ var startCmd = &cobra.Command{
156160
contextValuesToMap[k] = v
157161
}
158162

163+
headerToContextKeyMappings := make(map[string]string)
164+
for k, v := range viper.GetStringMapString(headerToContextKeyFlagName) {
165+
headerToContextKeyMappings[k] = v
166+
}
167+
159168
// Build Runtime -----------------------------------------------------------
160169
rt, err := runtime.FromConfig(logger, Version, runtime.Config{
161-
CORS: viper.GetStringSlice(corsFlagName),
162-
MetricExporter: viper.GetString(metricsExporter),
163-
ManagementPort: viper.GetUint16(managementPortFlagName),
164-
OfrepServicePort: viper.GetUint16(ofrepPortFlagName),
165-
OtelCollectorURI: viper.GetString(otelCollectorURI),
166-
OtelCertPath: viper.GetString(otelCertPathFlagName),
167-
OtelKeyPath: viper.GetString(otelKeyPathFlagName),
168-
OtelReloadInterval: viper.GetDuration(otelReloadIntervalFlagName),
169-
OtelCAPath: viper.GetString(otelCAPathFlagName),
170-
ServiceCertPath: viper.GetString(serverCertPathFlagName),
171-
ServiceKeyPath: viper.GetString(serverKeyPathFlagName),
172-
ServicePort: viper.GetUint16(portFlagName),
173-
ServiceSocketPath: viper.GetString(socketPathFlagName),
174-
SyncServicePort: viper.GetUint16(syncPortFlagName),
175-
SyncServiceSocketPath: viper.GetString(syncSocketPathFlagName),
176-
SyncProviders: syncProviders,
177-
ContextValues: contextValuesToMap,
170+
CORS: viper.GetStringSlice(corsFlagName),
171+
MetricExporter: viper.GetString(metricsExporter),
172+
ManagementPort: viper.GetUint16(managementPortFlagName),
173+
OfrepServicePort: viper.GetUint16(ofrepPortFlagName),
174+
OtelCollectorURI: viper.GetString(otelCollectorURI),
175+
OtelCertPath: viper.GetString(otelCertPathFlagName),
176+
OtelKeyPath: viper.GetString(otelKeyPathFlagName),
177+
OtelReloadInterval: viper.GetDuration(otelReloadIntervalFlagName),
178+
OtelCAPath: viper.GetString(otelCAPathFlagName),
179+
ServiceCertPath: viper.GetString(serverCertPathFlagName),
180+
ServiceKeyPath: viper.GetString(serverKeyPathFlagName),
181+
ServicePort: viper.GetUint16(portFlagName),
182+
ServiceSocketPath: viper.GetString(socketPathFlagName),
183+
SyncServicePort: viper.GetUint16(syncPortFlagName),
184+
SyncServiceSocketPath: viper.GetString(syncSocketPathFlagName),
185+
SyncProviders: syncProviders,
186+
ContextValues: contextValuesToMap,
187+
HeaderToContextKeyMappings: headerToContextKeyMappings,
178188
})
179189
if err != nil {
180190
rtLogger.Fatal(err.Error())

flagd/pkg/runtime/from_config.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ type Config struct {
4242
SyncProviders []sync.SourceConfig
4343
CORS []string
4444

45-
ContextValues map[string]any
45+
ContextValues map[string]any
46+
HeaderToContextKeyMappings map[string]string
4647
}
4748

4849
// FromConfig builds a runtime from startup configurations
@@ -106,6 +107,7 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
106107
Port: config.OfrepServicePort,
107108
},
108109
config.ContextValues,
110+
config.HeaderToContextKeyMappings,
109111
)
110112
if err != nil {
111113
return nil, fmt.Errorf("error creating ofrep service")
@@ -146,15 +148,16 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime,
146148
OfrepService: ofrepService,
147149
Service: connectService,
148150
ServiceConfig: service.Configuration{
149-
Port: config.ServicePort,
150-
ManagementPort: config.ManagementPort,
151-
ServiceName: svcName,
152-
KeyPath: config.ServiceKeyPath,
153-
CertPath: config.ServiceCertPath,
154-
SocketPath: config.ServiceSocketPath,
155-
CORS: config.CORS,
156-
Options: options,
157-
ContextValues: config.ContextValues,
151+
Port: config.ServicePort,
152+
ManagementPort: config.ManagementPort,
153+
ServiceName: svcName,
154+
KeyPath: config.ServiceKeyPath,
155+
CertPath: config.ServiceCertPath,
156+
SocketPath: config.ServiceSocketPath,
157+
CORS: config.CORS,
158+
Options: options,
159+
ContextValues: config.ContextValues,
160+
HeaderToContextKeyMappings: config.HeaderToContextKeyMappings,
158161
},
159162
SyncImpl: iSyncs,
160163
}, nil

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listene
172172
s.eventingConfiguration,
173173
s.metrics,
174174
svcConf.ContextValues,
175+
svcConf.HeaderToContextKeyMappings,
175176
)
176177

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

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package service
33
import (
44
"context"
55
"fmt"
6+
"net/http"
67
"time"
78

89
schemaV1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/schema/v1"
@@ -72,7 +73,7 @@ func (s *OldFlagEvaluationService) ResolveAll(
7273
Flags: make(map[string]*schemaV1.AnyFlag),
7374
}
7475

75-
values, _, err := s.eval.ResolveAllValues(sCtx, reqID, mergeContexts(req.Msg.GetContext().AsMap(), s.contextValues))
76+
values, _, err := s.eval.ResolveAllValues(sCtx, reqID, mergeContexts(req.Msg.GetContext().AsMap(), s.contextValues, req.Header(), make(map[string]string)))
7677
if err != nil {
7778
s.logger.WarnWithID(reqID, fmt.Sprintf("error resolving all flags: %v", err))
7879
return nil, fmt.Errorf("error resolving flags. Tracking ID: %s", reqID)
@@ -179,11 +180,13 @@ func (s *OldFlagEvaluationService) ResolveBoolean(
179180
sCtx,
180181
s.logger,
181182
s.eval.ResolveBooleanValue,
183+
req.Header(),
182184
req.Msg.GetFlagKey(),
183185
req.Msg.GetContext(),
184186
&booleanResponse{schemaV1Resp: res},
185187
s.metrics,
186188
s.contextValues,
189+
make(map[string]string),
187190
)
188191
if err != nil {
189192
span.RecordError(err)
@@ -206,11 +209,13 @@ func (s *OldFlagEvaluationService) ResolveString(
206209
sCtx,
207210
s.logger,
208211
s.eval.ResolveStringValue,
212+
req.Header(),
209213
req.Msg.GetFlagKey(),
210214
req.Msg.GetContext(),
211215
&stringResponse{schemaV1Resp: res},
212216
s.metrics,
213217
s.contextValues,
218+
make(map[string]string),
214219
)
215220
if err != nil {
216221
span.RecordError(err)
@@ -233,11 +238,13 @@ func (s *OldFlagEvaluationService) ResolveInt(
233238
sCtx,
234239
s.logger,
235240
s.eval.ResolveIntValue,
241+
req.Header(),
236242
req.Msg.GetFlagKey(),
237243
req.Msg.GetContext(),
238244
&intResponse{schemaV1Resp: res},
239245
s.metrics,
240246
s.contextValues,
247+
make(map[string]string),
241248
)
242249
if err != nil {
243250
span.RecordError(err)
@@ -260,11 +267,13 @@ func (s *OldFlagEvaluationService) ResolveFloat(
260267
sCtx,
261268
s.logger,
262269
s.eval.ResolveFloatValue,
270+
req.Header(),
263271
req.Msg.GetFlagKey(),
264272
req.Msg.GetContext(),
265273
&floatResponse{schemaV1Resp: res},
266274
s.metrics,
267275
s.contextValues,
276+
make(map[string]string),
268277
)
269278
if err != nil {
270279
span.RecordError(err)
@@ -287,11 +296,13 @@ func (s *OldFlagEvaluationService) ResolveObject(
287296
sCtx,
288297
s.logger,
289298
s.eval.ResolveObjectValue,
299+
req.Header(),
290300
req.Msg.GetFlagKey(),
291301
req.Msg.GetContext(),
292302
&objectResponse{schemaV1Resp: res},
293303
s.metrics,
294304
s.contextValues,
305+
make(map[string]string),
295306
)
296307
if err != nil {
297308
span.RecordError(err)
@@ -301,28 +312,34 @@ func (s *OldFlagEvaluationService) ResolveObject(
301312
return res, err
302313
}
303314

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 {
315+
// mergeContexts combines context values from headers, static context (from cli) and request context.
316+
// highest priority > header-context-from-cli > static-context-from-cli > request-context > lowest priority
317+
func mergeContexts(reqCtx, configFlagsCtx map[string]any, headers http.Header, headerToContextKeyMappings map[string]string) map[string]any {
307318
merged := make(map[string]any)
308319
for k, v := range reqCtx {
309320
merged[k] = v
310321
}
311322
for k, v := range configFlagsCtx {
312323
merged[k] = v
313324
}
325+
for header, contextKey := range headerToContextKeyMappings {
326+
if values, ok := headers[header]; ok {
327+
merged[contextKey] = values[0]
328+
}
329+
}
314330
return merged
315331
}
316332

317333
// resolve is a generic flag resolver
318-
func resolve[T constraints](ctx context.Context, logger *logger.Logger, resolver resolverSignature[T], flagKey string,
334+
func resolve[T constraints](ctx context.Context, logger *logger.Logger, resolver resolverSignature[T], header http.Header, flagKey string,
319335
evaluationContext *structpb.Struct, resp response[T], metrics telemetry.IMetricsRecorder,
320-
configContextValues map[string]any,
336+
configContextValues map[string]any, configHeaderToContextKeyMappings map[string]string,
321337
) error {
322338
reqID := xid.New().String()
323339
defer logger.ClearFields(reqID)
324340

325-
mergedContext := mergeContexts(evaluationContext.AsMap(), configContextValues)
341+
mergedContext := mergeContexts(evaluationContext.AsMap(), configContextValues, header, configHeaderToContextKeyMappings)
342+
326343
logger.WriteFields(
327344
reqID,
328345
zap.String("flag-key", flagKey),

0 commit comments

Comments
 (0)