diff --git a/cmd/start.go b/cmd/start.go index 1790bfa1b..842be6589 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -37,7 +37,8 @@ func init() { flags.StringP(socketPathFlagName, "d", "", "Flagd socket path. "+ "With grpc the service will become available on this address. "+ "With http(s) the grpc-gateway proxy will use this address internally.") - flags.StringP(evaluatorFlagName, "e", "json", "Set an evaluator e.g. json") + flags.StringP(evaluatorFlagName, "e", "json", "Set an evaluator e.g. json, yaml/yml."+ + "Please note that yaml/yml and json evaluations work the same (yaml/yml files are converted to json internally)") flags.StringP(serverCertPathFlagName, "c", "", "Server side tls certificate path") flags.StringP(serverKeyPathFlagName, "k", "", "Server side tls key path") flags.StringToStringP(providerArgsFlagName, @@ -45,7 +46,8 @@ func init() { flags.StringSliceP( uriFlagName, "f", []string{}, "Set a sync provider uri to read data from, this can be a filepath,"+ "url or FeatureFlagConfiguration. Using multiple providers is supported however if"+ - "flag keys are duplicated across multiple sources it may lead to unexpected behavior ", + " flag keys are duplicated across multiple sources it may lead to unexpected behavior. "+ + "Please note that if you are using filepath, flagd only supports files with `.yaml/.yml/.json` extension.", ) flags.StringP( bearerTokenFlagName, "b", "", "Set a bearer token to use for remote sync") diff --git a/config/samples/example_flags.yaml b/config/samples/example_flags.yaml new file mode 100644 index 000000000..dac0f4586 --- /dev/null +++ b/config/samples/example_flags.yaml @@ -0,0 +1,88 @@ +flags: + myBoolFlag: + state: ENABLED + variants: + 'on': true + 'off': false + defaultVariant: 'on' + myStringFlag: + state: ENABLED + variants: + key1: val1 + key2: val2 + defaultVariant: key1 + myFloatFlag: + state: ENABLED + variants: + one: 1.23 + two: 2.34 + defaultVariant: one + myIntFlag: + state: ENABLED + variants: + one: 1 + two: 2 + defaultVariant: one + myObjectFlag: + state: ENABLED + variants: + object1: + key: val + object2: + key: true + defaultVariant: object1 + isColorYellow: + state: ENABLED + variants: + 'on': true + 'off': false + defaultVariant: 'off' + targeting: + if: + - "==": + - var: + - color + - yellow + - 'on' + - 'off' + fibAlgo: + variants: + recursive: recursive + memo: memo + loop: loop + binet: binet + defaultVariant: recursive + state: ENABLED + targeting: + if: + - "$ref": emailWithFaas + - binet + - null + headerColor: + variants: + red: "#FF0000" + blue: "#0000FF" + green: "#00FF00" + yellow: "#FFFF00" + defaultVariant: red + state: ENABLED + targeting: + if: + - "$ref": emailWithFaas + - fractionalEvaluation: + - email + - - red + - 25 + - - blue + - 25 + - - green + - 25 + - - yellow + - 25 + - null +"$evaluators": + emailWithFaas: + in: + - "@faas.com" + - var: + - email \ No newline at end of file diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 843d98861..6d075a99c 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -6,17 +6,18 @@ highest priority, followed by environment variables and finally config file. Supported flags are as follows (result of running `./flagd start --help`): ``` - -b, --bearer-token string Set a bearer token to use for remote sync - -e, --evaluator string Set an evaluator e.g. json (default "json") - -h, --help help for start - -p, --port int32 Port to listen on (default 8013) - -m, --metrics-port int32 Port to set up metrics listener on (default 8014) - -c, --server-cert-path string Server side tls certificate path - -k, --server-key-path string Server side tls key path - -a, --sync-provider-args Sync provider arguments as key values separated by = - -d, --socket-path string Set the flagd socket path. - -f, --uri strings Set a sync provider uri to read data from this can be a filepath, url or reference to a kubernetes custom resource. Using multiple providers is supported, however if flag keys are duplicated across multiple sources it may lead to unexpected behavior. - -C, --cors-origin strings Set a CORS allow origin header, setting "*" will allow all origins (by default CORS headers are not set) + -b, --bearer-token string Set a bearer token to use for remote sync + -C, --cors-origin strings CORS allowed origins, * will allow all origins + -e, --evaluator string Set an evaluator e.g. json, yaml/yml. Please note that yaml/yml and json evaluations work the same (yaml/yml files are converted to json internally) (default "json") + -h, --help help for start + -m, --metrics-port int32 Port to serve metrics on (default 8014) + -p, --port int32 Port to listen on (default 8013) + -c, --server-cert-path string Server side tls certificate path + -k, --server-key-path string Server side tls key path + -d, --socket-path string Flagd socket path. With grpc the service will become available on this address. With http(s) the grpc-gateway proxy will use this address internally. + -y, --sync-provider string DEPRECATED: Set a sync provider e.g. filepath or remote + -a, --sync-provider-args stringToString Sync provider arguments as key values separated by = (default []) + -f, --uri .yaml/.yml/.json Set a sync provider uri to read data from, this can be a filepath,url or FeatureFlagConfiguration. Using multiple providers is supported however if flag keys are duplicated across multiple sources it may lead to unexpected behavior. Please note that if you are using filepath, flagd only supports files with .yaml/.yml/.json extension. ``` Environment variable keys are uppercased, prefixed with `FLAGD_` and all `-` are replaced with `_`. For example, diff --git a/go.mod b/go.mod index 2237ab7d1..8b8ce414b 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.25.4 k8s.io/apimachinery v0.25.4 k8s.io/client-go v0.25.4 @@ -97,7 +98,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.25.4 // indirect k8s.io/component-base v0.25.4 // indirect k8s.io/klog/v2 v2.80.1 // indirect diff --git a/pkg/runtime/from_config.go b/pkg/runtime/from_config.go index cad50bcf6..97503c997 100644 --- a/pkg/runtime/from_config.go +++ b/pkg/runtime/from_config.go @@ -62,7 +62,7 @@ func (r *Runtime) setService(logger *logger.Logger) { func (r *Runtime) setEvaluatorFromConfig(logger *logger.Logger) error { switch r.config.Evaluator { - case "json": + case "yaml", "yml", "json": r.Evaluator = eval.NewJSONEvaluator(logger) default: return errors.New("no evaluator set") @@ -84,6 +84,8 @@ func (r *Runtime) setSyncImplFromConfig(logger *logger.Logger) error { zap.String("sync", "filepath"), ), ProviderArgs: r.config.ProviderArgs, + // evaluator here is file type: `json`, `yaml` etc., + FileType: r.config.Evaluator, }) rtLogger.Debug(fmt.Sprintf("Using filepath sync-provider for %q", uri)) case regCrd.Match(uriB): diff --git a/pkg/sync/filepath_sync.go b/pkg/sync/filepath_sync.go index e3da3910d..d7b6018f9 100644 --- a/pkg/sync/filepath_sync.go +++ b/pkg/sync/filepath_sync.go @@ -2,10 +2,13 @@ package sync import ( "context" + "encoding/json" "errors" "fmt" "os" + "gopkg.in/yaml.v3" + "github.com/fsnotify/fsnotify" "github.com/open-feature/flagd/pkg/logger" ) @@ -14,6 +17,8 @@ type FilePathSync struct { URI string Logger *logger.Logger ProviderArgs ProviderArgs + // FileType indicates the file type e.g., json, yaml/yml etc., + FileType string } func (fs *FilePathSync) Source() string { @@ -28,7 +33,15 @@ func (fs *FilePathSync) Fetch(_ context.Context) (string, error) { if err != nil { return "", err } - return string(rawFile), nil + + switch fs.FileType { + case "yaml", "yml": + return yamlToJSON(rawFile) + case "json": + return string(rawFile), nil + default: + return "", fmt.Errorf("filepath extension '%v' is not supported", fs.FileType) + } } func (fs *FilePathSync) Notify(ctx context.Context, w chan<- INotify) { @@ -90,3 +103,22 @@ func (fs *FilePathSync) Notify(ctx context.Context, w chan<- INotify) { w <- &Notifier{Event: Event[DefaultEventType]{DefaultEventTypeReady}} // signal readiness to the caller <-ctx.Done() } + +// yamlToJSON is a generic helper function to convert +// yaml to json +func yamlToJSON(rawFile []byte) (string, error) { + var ms map[string]interface{} + // yaml.Unmarshal unmarshals to map[interface]interface{} + if err := yaml.Unmarshal(rawFile, &ms); err != nil { + return "", fmt.Errorf("unmarshal yaml: %w", err) + } + + // Adding spaces here because our evaluator transposer function + // doesn't understand json without indentations quite well + r, err := json.MarshalIndent(ms, "", " ") + if err != nil { + return "", fmt.Errorf("convert yaml to json: %w", err) + } + + return string(r), err +} diff --git a/pkg/sync/filepath_sync_test.go b/pkg/sync/filepath_sync_test.go index 2f1cf295f..5966cf3bb 100644 --- a/pkg/sync/filepath_sync_test.go +++ b/pkg/sync/filepath_sync_test.go @@ -117,8 +117,9 @@ func TestFilePathSync_Fetch(t *testing.T) { }{ "success": { fpSync: sync.FilePathSync{ - URI: fmt.Sprintf("%s/%s", dirName, fetchFileName), - Logger: logger.NewLogger(nil, false), + URI: fmt.Sprintf("%s/%s", dirName, fetchFileName), + Logger: logger.NewLogger(nil, false), + FileType: "json", // this is the default }, handleResponse: func(t *testing.T, fetched string, err error) { if err != nil { @@ -132,8 +133,9 @@ func TestFilePathSync_Fetch(t *testing.T) { }, "not found": { fpSync: sync.FilePathSync{ - URI: fmt.Sprintf("%s/%s", dirName, "not_found"), - Logger: logger.NewLogger(nil, false), + URI: fmt.Sprintf("%s/%s", dirName, "not_found"), + Logger: logger.NewLogger(nil, false), + FileType: "json", }, handleResponse: func(t *testing.T, fetched string, err error) { if err == nil {