diff --git a/core/pkg/sync/builder/syncbuilder.go b/core/pkg/sync/builder/syncbuilder.go
index a923a331f..d5e6a1a9f 100644
--- a/core/pkg/sync/builder/syncbuilder.go
+++ b/core/pkg/sync/builder/syncbuilder.go
@@ -5,7 +5,6 @@ import (
"net/http"
"os"
"regexp"
- msync "sync"
"time"
"github.com/open-feature/flagd/core/pkg/logger"
@@ -26,6 +25,8 @@ import (
const (
syncProviderFile = "file"
+ syncProviderFsNotify = "fsnotify"
+ syncProviderFileInfo = "fileinfo"
syncProviderGrpc = "grpc"
syncProviderKubernetes = "kubernetes"
syncProviderHTTP = "http"
@@ -91,8 +92,13 @@ func (sb *SyncBuilder) SyncsFromConfig(sourceConfigs []sync.SourceConfig, logger
func (sb *SyncBuilder) syncFromConfig(sourceConfig sync.SourceConfig, logger *logger.Logger) (sync.ISync, error) {
switch sourceConfig.Provider {
case syncProviderFile:
- logger.Debug(fmt.Sprintf("using filepath sync-provider for: %q", sourceConfig.URI))
return sb.newFile(sourceConfig.URI, logger), nil
+ case syncProviderFsNotify:
+ logger.Debug(fmt.Sprintf("using fsnotify sync-provider for: %q", sourceConfig.URI))
+ return sb.newFsNotify(sourceConfig.URI, logger), nil
+ case syncProviderFileInfo:
+ logger.Debug(fmt.Sprintf("using fileinfo sync-provider for: %q", sourceConfig.URI))
+ return sb.newFileInfo(sourceConfig.URI, logger), nil
case syncProviderKubernetes:
logger.Debug(fmt.Sprintf("using kubernetes sync-provider for: %s", sourceConfig.URI))
return sb.newK8s(sourceConfig.URI, logger)
@@ -107,20 +113,46 @@ func (sb *SyncBuilder) syncFromConfig(sourceConfig sync.SourceConfig, logger *lo
return sb.newGcs(sourceConfig, logger), nil
default:
- return nil, fmt.Errorf("invalid sync provider: %s, must be one of with '%s', '%s', '%s' or '%s'",
- sourceConfig.Provider, syncProviderFile, syncProviderKubernetes, syncProviderHTTP, syncProviderKubernetes)
+ return nil, fmt.Errorf("invalid sync provider: %s, must be one of with '%s', '%s', '%s', %s', '%s' or '%s'",
+ sourceConfig.Provider, syncProviderFile, syncProviderFsNotify, syncProviderFileInfo,
+ syncProviderKubernetes, syncProviderHTTP, syncProviderKubernetes)
}
}
+// newFile returns an fsinfo sync if we are in k8s or fileinfo if not
func (sb *SyncBuilder) newFile(uri string, logger *logger.Logger) *file.Sync {
- return &file.Sync{
- URI: regFile.ReplaceAllString(uri, ""),
- Logger: logger.WithFields(
+ switch os.Getenv("KUBERNETES_SERVICE_HOST") {
+ case "":
+ // no k8s service host env; use fileinfo
+ return sb.newFileInfo(uri, logger)
+ default:
+ // default to fsnotify
+ return sb.newFsNotify(uri, logger)
+ }
+}
+
+// return a new file.Sync that uses fsnotify under the hood
+func (sb *SyncBuilder) newFsNotify(uri string, logger *logger.Logger) *file.Sync {
+ return file.NewFileSync(
+ regFile.ReplaceAllString(uri, ""),
+ file.FSNOTIFY,
+ logger.WithFields(
zap.String("component", "sync"),
- zap.String("sync", "filepath"),
+ zap.String("sync", syncProviderFsNotify),
),
- Mux: &msync.RWMutex{},
- }
+ )
+}
+
+// return a new file.Sync that uses os.Stat/fs.FileInfo under the hood
+func (sb *SyncBuilder) newFileInfo(uri string, logger *logger.Logger) *file.Sync {
+ return file.NewFileSync(
+ regFile.ReplaceAllString(uri, ""),
+ file.FILEINFO,
+ logger.WithFields(
+ zap.String("component", "sync"),
+ zap.String("sync", syncProviderFileInfo),
+ ),
+ )
}
func (sb *SyncBuilder) newK8s(uri string, logger *logger.Logger) (*kubernetes.Sync, error) {
diff --git a/core/pkg/sync/file/fileinfo_watcher.go b/core/pkg/sync/file/fileinfo_watcher.go
new file mode 100644
index 000000000..21173ae36
--- /dev/null
+++ b/core/pkg/sync/file/fileinfo_watcher.go
@@ -0,0 +1,202 @@
+package file
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/open-feature/flagd/core/pkg/logger"
+)
+
+// Implements file.Watcher using a timer and os.FileInfo
+type fileInfoWatcher struct {
+ // Event Chan
+ evChan chan fsnotify.Event
+ // Errors Chan
+ erChan chan error
+ // logger
+ logger *logger.Logger
+ // Func to wrap os.Stat (injection point for test helpers)
+ statFunc func(string) (fs.FileInfo, error)
+ // thread-safe interface to underlying files we are watching
+ mu sync.RWMutex
+ watches map[string]fs.FileInfo // filename -> info
+}
+
+// NewFsNotifyWatcher returns a new fsNotifyWatcher
+func NewFileInfoWatcher(ctx context.Context, logger *logger.Logger) Watcher {
+ fiw := &fileInfoWatcher{
+ evChan: make(chan fsnotify.Event, 32),
+ erChan: make(chan error, 32),
+ statFunc: getFileInfo,
+ logger: logger,
+ watches: make(map[string]fs.FileInfo),
+ }
+ fiw.run(ctx, (1 * time.Second))
+ return fiw
+}
+
+// fileInfoWatcher explicitly implements file.Watcher
+var _ Watcher = &fileInfoWatcher{}
+
+// Close calls close on the underlying fsnotify.Watcher
+func (f *fileInfoWatcher) Close() error {
+ // close all channels and exit
+ close(f.evChan)
+ close(f.erChan)
+ return nil
+}
+
+// Add calls Add on the underlying fsnotify.Watcher
+func (f *fileInfoWatcher) Add(name string) error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ // exit early if name already exists
+ if _, ok := f.watches[name]; ok {
+ return nil
+ }
+
+ info, err := f.statFunc(name)
+ if err != nil {
+ return err
+ }
+
+ f.watches[name] = info
+
+ return nil
+}
+
+// Remove calls Remove on the underlying fsnotify.Watcher
+func (f *fileInfoWatcher) Remove(name string) error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ // no need to exit early, deleting non-existent key is a no-op
+ delete(f.watches, name)
+
+ return nil
+}
+
+// Watchlist calls watchlist on the underlying fsnotify.Watcher
+func (f *fileInfoWatcher) WatchList() []string {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+ out := []string{}
+ for name := range f.watches {
+ n := name
+ out = append(out, n)
+ }
+ return out
+}
+
+// Events returns the underlying watcher's Events chan
+func (f *fileInfoWatcher) Events() chan fsnotify.Event {
+ return f.evChan
+}
+
+// Errors returns the underlying watcher's Errors chan
+func (f *fileInfoWatcher) Errors() chan error {
+ return f.erChan
+}
+
+// run is a blocking function that starts the filewatcher's timer thread
+func (f *fileInfoWatcher) run(ctx context.Context, s time.Duration) {
+ // timer thread
+ go func() {
+ // execute update on the configured interval of time
+ ticker := time.NewTicker(s)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ if err := f.update(); err != nil {
+ f.erChan <- err
+ return
+ }
+ }
+ }
+ }()
+}
+
+func (f *fileInfoWatcher) update() error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ for path, info := range f.watches {
+ newInfo, err := f.statFunc(path)
+ if err != nil {
+ // if the file isn't there, it must have been removed
+ // fire off a remove event and remove it from the watches
+ if errors.Is(err, os.ErrNotExist) {
+ f.evChan <- fsnotify.Event{
+ Name: path,
+ Op: fsnotify.Remove,
+ }
+ delete(f.watches, path)
+ continue
+ }
+ return err
+ }
+
+ // if the new stat doesn't match the old stat, figure out what changed
+ if info != newInfo {
+ event := f.generateEvent(path, newInfo)
+ if event != nil {
+ f.evChan <- *event
+ }
+ f.watches[path] = newInfo
+ }
+ }
+ return nil
+}
+
+// generateEvent figures out what changed and generates an fsnotify.Event for it. (if we care)
+// file removal are handled above in the update() method
+func (f *fileInfoWatcher) generateEvent(path string, newInfo fs.FileInfo) *fsnotify.Event {
+ info := f.watches[path]
+ switch {
+ // new mod time is more recent than old mod time, generate a write event
+ case newInfo.ModTime().After(info.ModTime()):
+ return &fsnotify.Event{
+ Name: path,
+ Op: fsnotify.Write,
+ }
+ // the file modes changed, generate a chmod event
+ case info.Mode() != newInfo.Mode():
+ return &fsnotify.Event{
+ Name: path,
+ Op: fsnotify.Chmod,
+ }
+ // nothing changed that we care about
+ default:
+ return nil
+ }
+}
+
+// getFileInfo returns the fs.FileInfo for the given path
+func getFileInfo(path string) (fs.FileInfo, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("error from os.Open(%s): %w", path, err)
+ }
+
+ info, err := f.Stat()
+ if err != nil {
+ return info, fmt.Errorf("error from fs.Stat(%s): %w", path, err)
+ }
+
+ if err := f.Close(); err != nil {
+ return info, fmt.Errorf("err from fs.Close(%s): %w", path, err)
+ }
+
+ return info, nil
+}
diff --git a/core/pkg/sync/file/fileinfo_watcher_test.go b/core/pkg/sync/file/fileinfo_watcher_test.go
new file mode 100644
index 000000000..5917e9ab6
--- /dev/null
+++ b/core/pkg/sync/file/fileinfo_watcher_test.go
@@ -0,0 +1,248 @@
+package file
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/google/go-cmp/cmp"
+)
+
+func Test_fileInfoWatcher_Close(t *testing.T) {
+ tests := []struct {
+ name string
+ watcher *fileInfoWatcher
+ wantErr bool
+ }{
+ {
+ name: "all chans close",
+ watcher: makeTestWatcher(t, map[string]fs.FileInfo{}),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := tt.watcher.Close(); (err != nil) != tt.wantErr {
+ t.Errorf("fileInfoWatcher.Close() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if _, ok := (<-tt.watcher.Errors()); ok != false {
+ t.Error("fileInfoWatcher.Close() failed to close error chan")
+ }
+ if _, ok := (<-tt.watcher.Events()); ok != false {
+ t.Error("fileInfoWatcher.Close() failed to close events chan")
+ }
+ })
+ }
+}
+
+func Test_fileInfoWatcher_Add(t *testing.T) {
+ tests := []struct {
+ name string
+ watcher *fileInfoWatcher
+ add []string
+ want map[string]fs.FileInfo
+ wantErr bool
+ }{
+ {
+ name: "add one watch",
+ watcher: makeTestWatcher(t, map[string]fs.FileInfo{}),
+ add: []string{"/foo"},
+ want: map[string]fs.FileInfo{
+ "/foo": &mockFileInfo{},
+ },
+ },
+ }
+ for _, tt := range tests {
+ tt.watcher.statFunc = makeStatFunc(t, &mockFileInfo{})
+ t.Run(tt.name, func(t *testing.T) {
+ for _, path := range tt.add {
+ if err := tt.watcher.Add(path); (err != nil) != tt.wantErr {
+ t.Errorf("fileInfoWatcher.Add() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ }
+ if !cmp.Equal(tt.watcher.watches, tt.want, cmp.AllowUnexported(mockFileInfo{})) {
+ t.Errorf("fileInfoWatcher.Add(): want-, got+: %v ", cmp.Diff(tt.want, tt.watcher.watches))
+ }
+ })
+ }
+}
+
+func Test_fileInfoWatcher_Remove(t *testing.T) {
+ tests := []struct {
+ name string
+ watcher *fileInfoWatcher
+ removeThis string
+ want []string
+ }{{
+ name: "remove foo",
+ watcher: makeTestWatcher(t, map[string]fs.FileInfo{"foo": &mockFileInfo{}, "bar": &mockFileInfo{}}),
+ removeThis: "foo",
+ want: []string{"bar"},
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.watcher.Remove(tt.removeThis)
+ if err != nil {
+ t.Errorf("fileInfoWatcher.Remove() error = %v", err)
+ }
+ if !cmp.Equal(tt.watcher.WatchList(), tt.want) {
+ t.Errorf("fileInfoWatcher.Add(): want-, got+: %v ", cmp.Diff(tt.want, tt.watcher.WatchList()))
+ }
+ })
+ }
+}
+
+func Test_fileInfoWatcher_update(t *testing.T) {
+ tests := []struct {
+ name string
+ watcher *fileInfoWatcher
+ statFunc func(string) (fs.FileInfo, error)
+ wantErr bool
+ want *fsnotify.Event
+ }{
+ {
+ name: "chmod",
+ watcher: makeTestWatcher(t,
+ map[string]fs.FileInfo{
+ "foo": &mockFileInfo{
+ name: "foo",
+ mode: 0,
+ },
+ },
+ ),
+ statFunc: func(_ string) (fs.FileInfo, error) {
+ return &mockFileInfo{
+ name: "foo",
+ mode: 1,
+ }, nil
+ },
+ want: &fsnotify.Event{Name: "foo", Op: fsnotify.Chmod},
+ },
+ {
+ name: "write",
+ watcher: makeTestWatcher(t,
+ map[string]fs.FileInfo{
+ "foo": &mockFileInfo{
+ name: "foo",
+ modTime: time.Now().Local(),
+ },
+ },
+ ),
+ statFunc: func(_ string) (fs.FileInfo, error) {
+ return &mockFileInfo{
+ name: "foo",
+ modTime: (time.Now().Local().Add(5 * time.Minute)),
+ }, nil
+ },
+ want: &fsnotify.Event{Name: "foo", Op: fsnotify.Write},
+ },
+ {
+ name: "remove",
+ watcher: makeTestWatcher(t,
+ map[string]fs.FileInfo{
+ "foo": &mockFileInfo{
+ name: "foo",
+ },
+ },
+ ),
+ statFunc: func(_ string) (fs.FileInfo, error) {
+ return nil, fmt.Errorf("mock file-no-existy error: %w", os.ErrNotExist)
+ },
+ want: &fsnotify.Event{Name: "foo", Op: fsnotify.Remove},
+ },
+ {
+ name: "unknown error",
+ watcher: makeTestWatcher(t,
+ map[string]fs.FileInfo{
+ "foo": &mockFileInfo{
+ name: "foo",
+ },
+ },
+ ),
+ statFunc: func(_ string) (fs.FileInfo, error) {
+ return nil, errors.New("unhandled error")
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // set the statFunc
+ tt.watcher.statFunc = tt.statFunc
+ // run an update
+ // this also flexes fileinfowatcher.generateEvent()
+ err := tt.watcher.update()
+ if err != nil {
+ if tt.wantErr {
+ return
+ }
+ t.Errorf("fileInfoWatcher.update() unexpected error = %v, wantErr %v", err, tt.wantErr)
+ }
+ // slurp an event off the event chan
+ out := <-tt.watcher.Events()
+ if out != *tt.want {
+ t.Errorf("fileInfoWatcher.update() wanted %v, got %v", tt.want, out)
+ }
+ })
+ }
+}
+
+// Helpers
+
+// makeTestWatcher returns a pointer to a fileInfoWatcher suitable for testing
+func makeTestWatcher(t *testing.T, watches map[string]fs.FileInfo) *fileInfoWatcher {
+ t.Helper()
+
+ return &fileInfoWatcher{
+ evChan: make(chan fsnotify.Event, 512),
+ erChan: make(chan error, 512),
+ watches: watches,
+ }
+}
+
+// makeStateFunc returns an os.Stat wrapper that parrots back whatever its
+// constructor is given
+func makeStatFunc(t *testing.T, fi fs.FileInfo) func(string) (fs.FileInfo, error) {
+ t.Helper()
+ return func(_ string) (fs.FileInfo, error) {
+ return fi, nil
+ }
+}
+
+// mockFileInfo implements fs.FileInfo for mocks
+type mockFileInfo struct {
+ name string // base name of the file
+ size int64 // length in bytes for regular files; system-dependent for others
+ mode fs.FileMode // file mode bits
+ modTime time.Time // modification time
+}
+
+// explicitly impements fs.FileInfo
+var _ fs.FileInfo = &mockFileInfo{}
+
+func (mfi *mockFileInfo) Name() string {
+ return mfi.name
+}
+
+func (mfi *mockFileInfo) Size() int64 {
+ return mfi.size
+}
+
+func (mfi *mockFileInfo) Mode() fs.FileMode {
+ return mfi.mode
+}
+
+func (mfi *mockFileInfo) ModTime() time.Time {
+ return mfi.modTime
+}
+
+func (mfi *mockFileInfo) IsDir() bool {
+ return false
+}
+
+func (mfi *mockFileInfo) Sys() any {
+ return "foo"
+}
diff --git a/core/pkg/sync/file/filepath_sync.go b/core/pkg/sync/file/filepath_sync.go
index 6b2899a13..c67ca57c8 100644
--- a/core/pkg/sync/file/filepath_sync.go
+++ b/core/pkg/sync/file/filepath_sync.go
@@ -15,21 +15,38 @@ import (
"gopkg.in/yaml.v3"
)
+const (
+ FSNOTIFY = "fsnotify"
+ FILEINFO = "fileinfo"
+)
+
+type Watcher interface {
+ Close() error
+ Add(name string) error
+ Remove(name string) error
+ WatchList() []string
+ Events() chan fsnotify.Event
+ Errors() chan error
+}
+
type Sync struct {
URI string
Logger *logger.Logger
// FileType indicates the file type e.g., json, yaml/yml etc.,
fileType string
- watcher *fsnotify.Watcher
- ready bool
- Mux *msync.RWMutex
+ // watchType indicates how to watch the file FSNOTIFY|FILEINFO
+ watchType string
+ watcher Watcher
+ ready bool
+ Mux *msync.RWMutex
}
-func NewFileSync(uri string, logger *logger.Logger) *Sync {
+func NewFileSync(uri string, watchType string, logger *logger.Logger) *Sync {
return &Sync{
- URI: uri,
- Logger: logger,
- Mux: &msync.RWMutex{},
+ URI: uri,
+ watchType: watchType,
+ Logger: logger,
+ Mux: &msync.RWMutex{},
}
}
@@ -41,14 +58,24 @@ func (fs *Sync) ReSync(ctx context.Context, dataSync chan<- sync.DataSync) error
return nil
}
-func (fs *Sync) Init(_ context.Context) error {
+func (fs *Sync) Init(ctx context.Context) error {
fs.Logger.Info("Starting filepath sync notifier")
- w, err := fsnotify.NewWatcher()
- if err != nil {
- return fmt.Errorf("error creating filepath watcher: %w", err)
+
+ switch fs.watchType {
+ case FSNOTIFY, "":
+ w, err := NewFSNotifyWatcher()
+ if err != nil {
+ return fmt.Errorf("error creating fsnotify watcher: %w", err)
+ }
+ fs.watcher = w
+ case FILEINFO:
+ w := NewFileInfoWatcher(ctx, fs.Logger)
+ fs.watcher = w
+ default:
+ return fmt.Errorf("unknown watcher type: '%s'", fs.watchType)
}
- fs.watcher = w
- if err = fs.watcher.Add(fs.URI); err != nil {
+
+ if err := fs.watcher.Add(fs.URI); err != nil {
return fmt.Errorf("error adding watcher %s: %w", fs.URI, err)
}
return nil
@@ -74,7 +101,7 @@ func (fs *Sync) Sync(ctx context.Context, dataSync chan<- sync.DataSync) error {
fs.Logger.Info(fmt.Sprintf("watching filepath: %s", fs.URI))
for {
select {
- case event, ok := <-fs.watcher.Events:
+ case event, ok := <-fs.watcher.Events():
if !ok {
fs.Logger.Info("filepath notifier closed")
return errors.New("filepath notifier closed")
@@ -108,7 +135,7 @@ func (fs *Sync) Sync(ctx context.Context, dataSync chan<- sync.DataSync) error {
}
}
- case err, ok := <-fs.watcher.Errors:
+ case err, ok := <-fs.watcher.Errors():
if !ok {
fs.setReady(false)
return errors.New("watcher error")
diff --git a/core/pkg/sync/file/fsnotify_watcher.go b/core/pkg/sync/file/fsnotify_watcher.go
new file mode 100644
index 000000000..93c98ce1c
--- /dev/null
+++ b/core/pkg/sync/file/fsnotify_watcher.go
@@ -0,0 +1,67 @@
+package file
+
+import (
+ "fmt"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// Implements file.Watcher by wrapping fsnotify.Watcher
+// This is only necessary because fsnotify.Watcher directly exposes its Errors
+// and Events channels rather than returning them by method invocation
+type fsNotifyWatcher struct {
+ watcher *fsnotify.Watcher
+}
+
+// NewFsNotifyWatcher returns a new fsNotifyWatcher
+func NewFSNotifyWatcher() (Watcher, error) {
+ fsn, err := fsnotify.NewWatcher()
+ if err != nil {
+ return nil, fmt.Errorf("fsnotify: %w", err)
+ }
+ return &fsNotifyWatcher{
+ watcher: fsn,
+ }, nil
+}
+
+// explicitly implements file.Watcher
+var _ Watcher = &fsNotifyWatcher{}
+
+// Close calls close on the underlying fsnotify.Watcher
+func (f *fsNotifyWatcher) Close() error {
+ if err := f.watcher.Close(); err != nil {
+ return fmt.Errorf("fsnotify: %w", err)
+ }
+ return nil
+}
+
+// Add calls Add on the underlying fsnotify.Watcher
+func (f *fsNotifyWatcher) Add(name string) error {
+ if err := f.watcher.Add(name); err != nil {
+ return fmt.Errorf("fsnotify: %w", err)
+ }
+ return nil
+}
+
+// Remove calls Remove on the underlying fsnotify.Watcher
+func (f *fsNotifyWatcher) Remove(name string) error {
+ if err := f.watcher.Remove(name); err != nil {
+ return fmt.Errorf("fsnotify: %w", err)
+ }
+ return nil
+}
+
+// Watchlist calls watchlist on the underlying fsnotify.Watcher
+func (f *fsNotifyWatcher) WatchList() []string {
+ return f.watcher.WatchList()
+}
+
+// Events returns the underlying watcher's Events chan
+func (f *fsNotifyWatcher) Events() chan fsnotify.Event {
+ return f.watcher.Events
+}
+
+// Errors returns the underlying watcher's Errors chan
+func (f *fsNotifyWatcher) Errors() chan error {
+ return f.watcher.Errors
+}
diff --git a/docs/reference/sync-configuration.md b/docs/reference/sync-configuration.md
index 902463903..947d826a6 100644
--- a/docs/reference/sync-configuration.md
+++ b/docs/reference/sync-configuration.md
@@ -31,7 +31,7 @@ Alternatively, these configurations can be passed to flagd via config file, spec
| Field | Type | Note |
| ----------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| uri | required `string` | Flag configuration source of the sync |
-| provider | required `string` | Provider type - `file`, `kubernetes`, `http`, `grpc` or `gcs` |
+| provider | required `string` | Provider type - `file`, `fsnotify`, `fileinfo`, `kubernetes`, `http`, `grpc` or `gcs` |
| authHeader | optional `string` | Used for http sync; set this to include the complete `Authorization` header value for any authentication scheme (e.g., "Bearer token_here", "Basic base64_credentials", etc.). Cannot be used with `bearerToken` |
| bearerToken | optional `string` | (Deprecated) Used for http sync; token gets appended to `Authorization` header with [bearer schema](https://www.rfc-editor.org/rfc/rfc6750#section-2.1). Cannot be used with `authHeader` |
| interval | optional `uint32` | Used for http and gcs syncs; requests will be made at this interval. Defaults to 5 seconds. |
@@ -45,11 +45,20 @@ The `uri` field values **do not** follow the [URI patterns](#uri-patterns). The
from the `provider` field. Only exception is the remote provider where `http(s)://` is expected by default. Incorrect
URIs will result in a flagd start-up failure with errors from the respective sync provider implementation.
+The `file` provider type uses either an `fsnotify` notification (on systems that
+support it), or a timer-based poller that relies on `os.Stat` and `fs.FileInfo`.
+The moniker: `file` defaults to using `fsnotify` when flagd detects it is
+running in kubernetes and `fileinfo` in all other cases, but you may explicitly
+select either polling back-end by setting the provider value to either
+`fsnotify` or `fileinfo`.
+
Given below are example sync providers, startup command and equivalent config file definition:
Sync providers:
- `file` - config/samples/example_flags.json
+- `fsnotify` - config/samples/example_flags.json
+- `fileinfo` - config/samples/example_flags.json
- `http` -
- `https` -
- `kubernetes` - default/my-flag-config
@@ -62,6 +71,8 @@ Startup command:
```sh
./bin/flagd start
--sources='[{"uri":"config/samples/example_flags.json","provider":"file"},
+ {"uri":"config/samples/example_flags.json","provider":"fsnotify"},
+ {"uri":"config/samples/example_flags.json","provider":"fileinfo"},
{"uri":"http://my-flag-source.json","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"uri":"https://secure-remote/bearer-auth","provider":"http","authHeader":"Bearer bearer-dji34ld2l"},
{"uri":"https://secure-remote/basic-auth","provider":"http","authHeader":"Basic dXNlcjpwYXNz"},
@@ -78,6 +89,10 @@ Configuration file,
sources:
- uri: config/samples/example_flags.json
provider: file
+ - uri: config/samples/example_flags.json
+ provider: fsnotify
+ - uri: config/samples/example_flags.json
+ provider: fileinfo
- uri: http://my-flag-source.json
provider: http
bearerToken: bearer-dji34ld2l