Skip to content

Commit b49abf9

Browse files
authored
feat!: flagSetMetadata in OFREP/ResolveAll, core refactors (#1540)
⚠️ This PR brings a breaking change to the [flagd-core](https://pkg.go.dev/github.com/open-feature/flagd/core) library: the `IStore` interface now returns an additional value representing the flag set metadata. There are no breaking changes in flagd's behavior. Changes in flagd: - returns flag set metadata as metadata for error flags (best effort) - returns flag set metadata in OFREP and RPC calls - moves metadata merging logic to evaluator (all flags inherent flag set metadata, but can override, as as before but now reusable in flagd core) - removes duplicated flag set metadata keys when the same flag set metadata key exists in multiple sources ### To Test - requires `curl`, `grpcurl`, and `jq` #### RPC ```shell grpcurl -import-path /...../schemas/protobuf/flagd/evaluation/v1 -proto evaluation.proto -plaintext localhost:8013 flagd.evaluation.v1.Service/ResolveAll | jq ``` ### OFREP ```shell curl --location 'http://localhost:8016/ofrep/v1/evaluate/flags' --header 'Content-Type: application/json' --data '{"context": {"color": "yellow"}}' | jq ``` ### Sync ```shell grpcurl -import-path /...../schemas/protobuf/flagd/sync/v1/ -proto sync.proto -plaintext localhost:8015 flagd.sync.v1.FlagSyncService/FetchAllFlags | jq -r .flagConfiguration | jq ``` --------- Signed-off-by: Todd Baert <[email protected]>
1 parent 4281c6e commit b49abf9

30 files changed

+441
-989
lines changed

config/samples/example_flags.flagd.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
{
22
"$schema": "https://flagd.dev/schema/v0/flags.json",
3+
"metadata": {
4+
"flagSetId": "example",
5+
"version": "v1"
6+
},
37
"flags": {
48
"myBoolFlag": {
59
"state": "ENABLED",
610
"variants": {
711
"on": true,
812
"off": false
913
},
10-
"defaultVariant": "on"
14+
"defaultVariant": "on",
15+
"metadata": {
16+
"version": "v2"
17+
}
1118
},
1219
"myStringFlag": {
1320
"state": "ENABLED",

config/samples/example_flags_secondary.flagd.json

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"$schema": "https://flagd.dev/schema/v0/flags.json",
3+
"metadata": {
4+
"version": "v2"
5+
},
36
"flags": {
47
"myBoolFlag": {
58
"state": "ENABLED",

core/go.sum

-4
Original file line numberDiff line numberDiff line change
@@ -317,10 +317,6 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
317317
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
318318
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
319319
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
320-
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
321-
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
322-
golang.org/x/exp v0.0.0-20250128144449-3edf0e91c1ae h1:COZdc9Ut6wLq7MO9GIYxfZl4n4ScmgqQLoHocKXrxco=
323-
golang.org/x/exp v0.0.0-20250128144449-3edf0e91c1ae/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
324320
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
325321
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
326322
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

core/pkg/evaluator/fractional_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ func BenchmarkFractionalEvaluation(b *testing.B) {
587587
for name, tt := range tests {
588588
b.Run(name, func(b *testing.B) {
589589
log := logger.NewLogger(nil, false)
590-
je := NewJSON(log, &store.Flags{Flags: tt.flags.Flags})
590+
je := NewJSON(log, &store.State{Flags: tt.flags.Flags})
591591
for i := 0; i < b.N; i++ {
592592
value, variant, reason, _, err := resolve[string](
593593
ctx, reqID, tt.flagKey, tt.context, je.evaluateVariant)

core/pkg/evaluator/ievaluator.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package evaluator
33
import (
44
"context"
55

6+
"github.com/open-feature/flagd/core/pkg/model"
67
"github.com/open-feature/flagd/core/pkg/sync"
78
)
89

@@ -11,12 +12,12 @@ type AnyValue struct {
1112
Variant string
1213
Reason string
1314
FlagKey string
14-
Metadata map[string]interface{}
15+
Metadata model.Metadata
1516
Error error
1617
}
1718

1819
func NewAnyValue(
19-
value interface{}, variant string, reason string, flagKey string, metadata map[string]interface{},
20+
value interface{}, variant string, reason string, flagKey string, metadata model.Metadata,
2021
err error,
2122
) AnyValue {
2223
return AnyValue{
@@ -34,7 +35,7 @@ IEvaluator is an extension of IResolver, allowing storage updates and retrievals
3435
*/
3536
type IEvaluator interface {
3637
GetState() (string, error)
37-
SetState(payload sync.DataSync) (map[string]interface{}, bool, error)
38+
SetState(payload sync.DataSync) (model.Metadata, bool, error)
3839
IResolver
3940
}
4041

@@ -44,31 +45,31 @@ type IResolver interface {
4445
ctx context.Context,
4546
reqID string,
4647
flagKey string,
47-
context map[string]any) (value bool, variant string, reason string, metadata map[string]interface{}, err error)
48+
context map[string]any) (value bool, variant string, reason string, metadata model.Metadata, err error)
4849
ResolveStringValue(
4950
ctx context.Context,
5051
reqID string,
5152
flagKey string,
5253
context map[string]any) (
53-
value string, variant string, reason string, metadata map[string]interface{}, err error)
54+
value string, variant string, reason string, metadata model.Metadata, err error)
5455
ResolveIntValue(
5556
ctx context.Context,
5657
reqID string,
5758
flagKey string,
5859
context map[string]any) (
59-
value int64, variant string, reason string, metadata map[string]interface{}, err error)
60+
value int64, variant string, reason string, metadata model.Metadata, err error)
6061
ResolveFloatValue(
6162
ctx context.Context,
6263
reqID string,
6364
flagKey string,
6465
context map[string]any) (
65-
value float64, variant string, reason string, metadata map[string]interface{}, err error)
66+
value float64, variant string, reason string, metadata model.Metadata, err error)
6667
ResolveObjectValue(
6768
ctx context.Context,
6869
reqID string,
6970
flagKey string,
7071
context map[string]any) (
71-
value map[string]any, variant string, reason string, metadata map[string]interface{}, err error)
72+
value map[string]any, variant string, reason string, metadata model.Metadata, err error)
7273
ResolveAsAnyValue(
7374
ctx context.Context,
7475
reqID string,
@@ -77,5 +78,5 @@ type IResolver interface {
7778
ResolveAllValues(
7879
ctx context.Context,
7980
reqID string,
80-
context map[string]any) (values []AnyValue, err error)
81+
context map[string]any) (resolutions []AnyValue, metadata model.Metadata, err error)
8182
}

core/pkg/evaluator/json.go

+21-37
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ func WithEvaluator(name string, evalFunc func(interface{}, interface{}) interfac
6464

6565
// JSON evaluator
6666
type JSON struct {
67-
store *store.Flags
67+
store *store.State
6868
Logger *logger.Logger
6969
jsonEvalTracer trace.Tracer
7070
Resolver
7171
}
7272

73-
func NewJSON(logger *logger.Logger, s *store.Flags, opts ...JSONEvaluatorOption) *JSON {
73+
func NewJSON(logger *logger.Logger, s *store.State, opts ...JSONEvaluatorOption) *JSON {
7474
logger = logger.WithFields(
7575
zap.String("component", "evaluator"),
7676
zap.String("evaluator", "json"),
@@ -107,9 +107,9 @@ func (je *JSON) SetState(payload sync.DataSync) (map[string]interface{}, bool, e
107107
trace.WithAttributes(attribute.String("feature_flag.sync_type", payload.Type.String())))
108108
defer span.End()
109109

110-
var newFlags Flags
110+
var definition Definition
111111

112-
err := configToFlags(je.Logger, payload.FlagData, &newFlags)
112+
err := configToFlagDefinition(je.Logger, payload.FlagData, &definition)
113113
if err != nil {
114114
span.SetStatus(codes.Error, "flagSync error")
115115
span.RecordError(err)
@@ -119,15 +119,16 @@ func (je *JSON) SetState(payload sync.DataSync) (map[string]interface{}, bool, e
119119
var events map[string]interface{}
120120
var reSync bool
121121

122+
// TODO: We do not handle metadata in ADD/UPDATE operations. These are only relevant for grpc sync implementations.
122123
switch payload.Type {
123124
case sync.ALL:
124-
events, reSync = je.store.Merge(je.Logger, payload.Source, payload.Selector, newFlags.Flags)
125+
events, reSync = je.store.Merge(je.Logger, payload.Source, payload.Selector, definition.Flags, definition.Metadata)
125126
case sync.ADD:
126-
events = je.store.Add(je.Logger, payload.Source, payload.Selector, newFlags.Flags)
127+
events = je.store.Add(je.Logger, payload.Source, payload.Selector, definition.Flags)
127128
case sync.UPDATE:
128-
events = je.store.Update(je.Logger, payload.Source, payload.Selector, newFlags.Flags)
129+
events = je.store.Update(je.Logger, payload.Source, payload.Selector, definition.Flags)
129130
case sync.DELETE:
130-
events = je.store.DeleteFlags(je.Logger, payload.Source, newFlags.Flags)
131+
events = je.store.DeleteFlags(je.Logger, payload.Source, definition.Flags)
131132
default:
132133
return nil, false, fmt.Errorf("unsupported sync type: %d", payload.Type)
133134
}
@@ -156,14 +157,16 @@ func NewResolver(store store.IStore, logger *logger.Logger, jsonEvalTracer trace
156157
return Resolver{store: store, Logger: logger, tracer: jsonEvalTracer}
157158
}
158159

159-
func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) ([]AnyValue, error) {
160+
func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context map[string]any) ([]AnyValue,
161+
model.Metadata, error,
162+
) {
160163
_, span := je.tracer.Start(ctx, "resolveAll")
161164
defer span.End()
162165

163166
var err error
164-
allFlags, err := je.store.GetAll(ctx)
167+
allFlags, flagSetMetadata, err := je.store.GetAll(ctx)
165168
if err != nil {
166-
return nil, fmt.Errorf("error retreiving flags from the store: %w", err)
169+
return nil, flagSetMetadata, fmt.Errorf("error retreiving flags from the store: %w", err)
167170
}
168171

169172
values := []AnyValue{}
@@ -195,7 +198,7 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context
195198
values = append(values, NewAnyValue(value, variant, reason, flagKey, metadata, err))
196199
}
197200

198-
return values, nil
201+
return values, flagSetMetadata, nil
199202
}
200203

201204
func (je *Resolver) ResolveBooleanValue(
@@ -312,9 +315,7 @@ func resolve[T constraints](ctx context.Context, reqID string, key string, conte
312315
func (je *Resolver) evaluateVariant(ctx context.Context, reqID string, flagKey string, evalCtx map[string]any) (
313316
variant string, variants map[string]interface{}, reason string, metadata map[string]interface{}, err error,
314317
) {
315-
metadata = map[string]interface{}{}
316-
317-
flag, ok := je.store.Get(ctx, flagKey)
318+
flag, metadata, ok := je.store.Get(ctx, flagKey)
318319
if !ok {
319320
// flag not found
320321
je.Logger.DebugWithID(reqID, fmt.Sprintf("requested flag could not be found: %s", flagKey))
@@ -447,8 +448,8 @@ func loadAndCompileSchema(log *logger.Logger) *gojsonschema.Schema {
447448
return compiledSchema
448449
}
449450

450-
// configToFlags convert string configurations to flags and store them to pointer newFlags
451-
func configToFlags(log *logger.Logger, config string, newFlags *Flags) error {
451+
// configToFlagDefinition convert string configurations to flags and store them to pointer newFlags
452+
func configToFlagDefinition(log *logger.Logger, config string, definition *Definition) error {
452453
compiledSchema := loadAndCompileSchema(log)
453454

454455
flagStringLoader := gojsonschema.NewStringLoader(config)
@@ -467,33 +468,16 @@ func configToFlags(log *logger.Logger, config string, newFlags *Flags) error {
467468
return fmt.Errorf("transposing evaluators: %w", err)
468469
}
469470

470-
var configData ConfigWithMetadata
471-
err = json.Unmarshal([]byte(transposedConfig), &configData)
471+
err = json.Unmarshal([]byte(transposedConfig), &definition)
472472
if err != nil {
473473
return fmt.Errorf("unmarshalling provided configurations: %w", err)
474474
}
475475

476-
// Assign the flags from the unmarshalled config to the newFlags struct
477-
newFlags.Flags = configData.Flags
478-
479-
// Assign metadata as a map to each flag's metadata
480-
for key, flag := range newFlags.Flags {
481-
if flag.Metadata == nil {
482-
flag.Metadata = make(map[string]interface{})
483-
}
484-
for metaKey, metaValue := range configData.Metadata {
485-
if _, exists := flag.Metadata[metaKey]; !exists {
486-
flag.Metadata[metaKey] = metaValue
487-
}
488-
}
489-
newFlags.Flags[key] = flag
490-
}
491-
492-
return validateDefaultVariants(newFlags)
476+
return validateDefaultVariants(definition)
493477
}
494478

495479
// validateDefaultVariants returns an error if any of the default variants aren't valid
496-
func validateDefaultVariants(flags *Flags) error {
480+
func validateDefaultVariants(flags *Definition) error {
497481
for name, flag := range flags.Flags {
498482
if _, ok := flag.Variants[flag.DefaultVariant]; !ok {
499483
return fmt.Errorf(

core/pkg/evaluator/json_model.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type Evaluators struct {
1010
Evaluators map[string]json.RawMessage `json:"$evaluators"`
1111
}
1212

13-
type ConfigWithMetadata struct {
13+
type Definition struct {
1414
Flags map[string]model.Flag `json:"flags"`
1515
Metadata map[string]interface{} `json:"metadata"`
1616
}

0 commit comments

Comments
 (0)