From a77cafd6f12932aec4727ff46bcaa26940ece864 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Wed, 30 Dec 2020 16:17:37 +0200 Subject: [PATCH 1/9] Notification addition --- examples/06_notification/main.go | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 examples/06_notification/main.go diff --git a/examples/06_notification/main.go b/examples/06_notification/main.go new file mode 100644 index 00000000..c84923fe --- /dev/null +++ b/examples/06_notification/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/beatlabs/harvester" + "github.com/beatlabs/harvester/sync" +) + +type config struct { + IndexName sync.String `seed:"customers-v1"` + CacheRetention sync.Int64 `seed:"43200" env:"ENV_CACHE_RETENTION_SECONDS"` + LogLevel sync.String `seed:"DEBUG" flag:"loglevel"` +} + +func main() { + ctx, cnl := context.WithCancel(context.Background()) + defer cnl() + + err := os.Setenv("ENV_CACHE_RETENTION_SECONDS", "86400") + if err != nil { + log.Fatalf("failed to set env var: %v", err) + } + + cfg := config{} + + h, err := harvester.New(&cfg).Create() + if err != nil { + log.Fatalf("failed to create harvester: %v", err) + } + + err = h.Harvest(ctx) + if err != nil { + log.Fatalf("failed to harvest configuration: %v", err) + } + + log.Printf("Config : IndexName: %s, CacheRetention: %d, LogLevel: %s\n", cfg.IndexName.Get(), cfg.CacheRetention.Get(), cfg.LogLevel.Get()) +} From 505fcd860a30e1144a46513628eaa7730e48b813 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Wed, 30 Dec 2020 16:17:44 +0200 Subject: [PATCH 2/9] Notification addition --- config/config.go | 18 +++++++++++++++--- config/config_test.go | 13 ++++++++++--- config/custom_type_test.go | 4 ++-- config/parser.go | 14 +++++++------- examples/06_notification/main.go | 25 +++++++++++++++++++------ harvester.go | 15 ++++++++++++++- monitor/monitor_test.go | 11 ++++++----- seed/seed_test.go | 16 ++++++++-------- 8 files changed, 81 insertions(+), 35 deletions(-) diff --git a/config/config.go b/config/config.go index dc1fe024..699e25a1 100644 --- a/config/config.go +++ b/config/config.go @@ -40,16 +40,18 @@ type Field struct { version uint64 structField CfgType sources map[Source]string + chNotify chan<- string } // newField constructor. -func newField(prefix string, fld reflect.StructField, val reflect.Value) *Field { +func newField(prefix string, fld reflect.StructField, val reflect.Value, chNotify chan<- string) *Field { f := &Field{ name: prefix + fld.Name, tp: fld.Type.Name(), version: 0, structField: val.Addr().Interface().(CfgType), sources: make(map[Source]string), + chNotify: chNotify, } for _, tag := range sourceTags { @@ -94,27 +96,37 @@ func (f *Field) Set(value string, version uint64) error { return nil } + prevValue := f.structField.String() + if err := f.structField.SetString(value); err != nil { return err } f.version = version log.Infof("field %q updated with value %q, version: %d", f.name, f, version) + f.sendNotification(prevValue, value) return nil } +func (f *Field) sendNotification(prev string, current string) { + if f.chNotify == nil { + return + } + f.chNotify <- fmt.Sprintf("field [%s] of type [%s] changed from [%s] to [%s]", f.name, f.tp, prev, current) +} + // Config manages configuration and handles updates on the values. type Config struct { Fields []*Field } // New creates a new monitor. -func New(cfg interface{}) (*Config, error) { +func New(cfg interface{}, chNotify chan<- string) (*Config, error) { if cfg == nil { return nil, errors.New("configuration is nil") } - ff, err := newParser().ParseCfg(cfg) + ff, err := newParser().ParseCfg(cfg, chNotify) if err != nil { return nil, err } diff --git a/config/config_test.go b/config/config_test.go index c33c32ed..911d99f2 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,7 +10,7 @@ import ( func TestField_Set(t *testing.T) { c := testConfig{} - cfg, err := New(&c) + cfg, err := New(&c, nil) require.NoError(t, err) cfg.Fields[0].version = 2 type args struct { @@ -63,7 +63,7 @@ func TestNew(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := New(tt.args.cfg) + got, err := New(tt.args.cfg, nil) if tt.wantErr { assert.Error(t, err) assert.Nil(t, got) @@ -97,20 +97,27 @@ func assertField(t *testing.T, fld *Field, name, typ string, sources map[Source] func TestConfig_Set(t *testing.T) { c := testConfig{} - cfg, err := New(&c) + chNotify := make(chan string, 1) + cfg, err := New(&c, chNotify) require.NoError(t, err) err = cfg.Fields[0].Set("John Doe", 1) assert.NoError(t, err) + assert.Equal(t, "field [Name] of type [String] changed from [] to [John Doe]", <-chNotify) err = cfg.Fields[1].Set("18", 1) assert.NoError(t, err) + assert.Equal(t, "field [Age] of type [Int64] changed from [0] to [18]", <-chNotify) err = cfg.Fields[2].Set("99.9", 1) assert.NoError(t, err) + assert.Equal(t, "field [Balance] of type [Float64] changed from [0.000000] to [99.9]", <-chNotify) err = cfg.Fields[3].Set("true", 1) assert.NoError(t, err) + assert.Equal(t, "field [HasJob] of type [Bool] changed from [false] to [true]", <-chNotify) err = cfg.Fields[4].Set("6000", 1) assert.NoError(t, err) + assert.Equal(t, "field [PositionSalary] of type [Int64] changed from [0] to [6000]", <-chNotify) err = cfg.Fields[5].Set("baz", 1) assert.NoError(t, err) + assert.Equal(t, "field [LevelOneLevelTwoDeepField] of type [String] changed from [] to [baz]", <-chNotify) assert.Equal(t, "John Doe", c.Name.Get()) assert.Equal(t, int64(18), c.Age.Get()) assert.Equal(t, 99.9, c.Balance.Get()) diff --git a/config/custom_type_test.go b/config/custom_type_test.go index 17d050c5..9f415138 100644 --- a/config/custom_type_test.go +++ b/config/custom_type_test.go @@ -12,7 +12,7 @@ import ( func TestCustomField(t *testing.T) { c := &testConfig{} - cfg, err := config.New(c) + cfg, err := config.New(c, nil) assert.NoError(t, err) err = cfg.Fields[0].Set("expected", 1) assert.NoError(t, err) @@ -24,7 +24,7 @@ func TestCustomField(t *testing.T) { func TestErrorValidationOnCustomField(t *testing.T) { c := &testConfig{} - cfg, err := config.New(c) + cfg, err := config.New(c, nil) assert.NoError(t, err) err = cfg.Fields[0].Set("not_expected", 1) assert.Error(t, err) diff --git a/config/parser.go b/config/parser.go index 6eec6a28..76128486 100644 --- a/config/parser.go +++ b/config/parser.go @@ -22,7 +22,7 @@ func newParser() *parser { return &parser{} } -func (p *parser) ParseCfg(cfg interface{}) ([]*Field, error) { +func (p *parser) ParseCfg(cfg interface{}, chNotify chan<- string) ([]*Field, error) { p.dups = make(map[Source]string) tp := reflect.TypeOf(cfg) @@ -30,10 +30,10 @@ func (p *parser) ParseCfg(cfg interface{}) ([]*Field, error) { return nil, errors.New("configuration should be a pointer type") } - return p.getFields("", tp.Elem(), reflect.ValueOf(cfg).Elem()) + return p.getFields("", tp.Elem(), reflect.ValueOf(cfg).Elem(), chNotify) } -func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([]*Field, error) { +func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value, chNotify chan<- string) ([]*Field, error) { var ff []*Field for i := 0; i < tp.NumField(); i++ { @@ -46,13 +46,13 @@ func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([ switch typ { case typeField: - fld, err := p.createField(prefix, f, val.Field(i)) + fld, err := p.createField(prefix, f, val.Field(i), chNotify) if err != nil { return nil, err } ff = append(ff, fld) case typeStruct: - nested, err := p.getFields(prefix+f.Name, f.Type, val.Field(i)) + nested, err := p.getFields(prefix+f.Name, f.Type, val.Field(i), chNotify) if err != nil { return nil, err } @@ -62,8 +62,8 @@ func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([ return ff, nil } -func (p *parser) createField(prefix string, f reflect.StructField, val reflect.Value) (*Field, error) { - fld := newField(prefix, f, val) +func (p *parser) createField(prefix string, f reflect.StructField, val reflect.Value, chNotify chan<- string) (*Field, error) { + fld := newField(prefix, f, val, chNotify) value, ok := fld.Sources()[SourceConsul] if ok { diff --git a/examples/06_notification/main.go b/examples/06_notification/main.go index c84923fe..ae8bf2fe 100644 --- a/examples/06_notification/main.go +++ b/examples/06_notification/main.go @@ -4,15 +4,16 @@ import ( "context" "log" "os" + "sync" "github.com/beatlabs/harvester" - "github.com/beatlabs/harvester/sync" + harvestersync "github.com/beatlabs/harvester/sync" ) type config struct { - IndexName sync.String `seed:"customers-v1"` - CacheRetention sync.Int64 `seed:"43200" env:"ENV_CACHE_RETENTION_SECONDS"` - LogLevel sync.String `seed:"DEBUG" flag:"loglevel"` + IndexName harvestersync.String `seed:"customers-v1"` + CacheRetention harvestersync.Int64 `seed:"43200" env:"ENV_CACHE_RETENTION_SECONDS"` + LogLevel harvestersync.String `seed:"DEBUG" flag:"loglevel"` } func main() { @@ -25,8 +26,18 @@ func main() { } cfg := config{} - - h, err := harvester.New(&cfg).Create() + chNotify := make(chan string) + wg := sync.WaitGroup{} + wg.Add(1) + + go func() { + for change := range chNotify { + log.Printf("notification: " + change) + } + wg.Done() + }() + + h, err := harvester.NewWithNotification(&cfg, chNotify).Create() if err != nil { log.Fatalf("failed to create harvester: %v", err) } @@ -37,4 +48,6 @@ func main() { } log.Printf("Config : IndexName: %s, CacheRetention: %d, LogLevel: %s\n", cfg.IndexName.Get(), cfg.CacheRetention.Get(), cfg.LogLevel.Get()) + close(chNotify) + wg.Wait() } diff --git a/harvester.go b/harvester.go index 3678698f..ee928e3f 100644 --- a/harvester.go +++ b/harvester.go @@ -56,7 +56,20 @@ type Builder struct { // New constructor. func New(cfg interface{}) *Builder { b := &Builder{} - c, err := config.New(cfg) + c, err := config.New(cfg, nil) + if err != nil { + b.err = err + return b + } + b.cfg = c + b.seedParams = []seed.Param{} + return b +} + +// NewWithNotification constructor. +func NewWithNotification(cfg interface{}, chNotify chan string) *Builder { + b := &Builder{} + c, err := config.New(cfg, chNotify) if err != nil { b.err = err return b diff --git a/monitor/monitor_test.go b/monitor/monitor_test.go index b1a68b80..91c40436 100644 --- a/monitor/monitor_test.go +++ b/monitor/monitor_test.go @@ -14,9 +14,10 @@ import ( ) func TestNew(t *testing.T) { - cfg, err := config.New(&testConfig{}) + cfg, err := config.New(&testConfig{}, nil) + require.NoError(t, err) + errCfg, err := config.New(&testConfig{}, nil) require.NoError(t, err) - errCfg, err := config.New(&testConfig{}) errCfg.Fields[3].Sources()[config.SourceConsul] = "/config/balance" require.NoError(t, err) watchers := []Watcher{&testWatcher{}} @@ -49,7 +50,7 @@ func TestNew(t *testing.T) { } func TestMonitor_Monitor_Error(t *testing.T) { - cfg, err := config.New(&testConfig{}) + cfg, err := config.New(&testConfig{}, nil) require.NoError(t, err) watchers := []Watcher{&testWatcher{}, &testWatcher{err: true}} mon, err := New(cfg, watchers...) @@ -60,7 +61,7 @@ func TestMonitor_Monitor_Error(t *testing.T) { func TestMonitor_Monitor(t *testing.T) { c := &testConfig{} - cfg, err := config.New(c) + cfg, err := config.New(c, nil) require.NoError(t, err) watchers := []Watcher{&testWatcher{}} mon, err := New(cfg, watchers...) @@ -88,7 +89,7 @@ type testWatcher struct { err bool } -func (tw *testWatcher) Watch(ctx context.Context, ch chan<- []*change.Change) error { +func (tw *testWatcher) Watch(_ context.Context, ch chan<- []*change.Change) error { if tw.err { return errors.New("TEST") } diff --git a/seed/seed_test.go b/seed/seed_test.go index 2c95cc64..d0933608 100644 --- a/seed/seed_test.go +++ b/seed/seed_test.go @@ -123,7 +123,7 @@ func TestSeeder_Seed_Flags(t *testing.T) { os.Args = append(os.Args, tC.extraCliArgs...) seeder := New() - cfg, err := config.New(tC.inputConfig) + cfg, err := config.New(tC.inputConfig, nil) require.NoError(t, err) err = seeder.Seed(cfg) @@ -144,23 +144,23 @@ func TestSeeder_Seed(t *testing.T) { require.NoError(t, os.Setenv("ENV_WORK_HOURS", "9h")) c := testConfig{} - goodCfg, err := config.New(&c) + goodCfg, err := config.New(&c, nil) require.NoError(t, err) prmSuccess, err := NewParam(config.SourceConsul, &testConsulGet{}) require.NoError(t, err) - invalidIntCfg, err := config.New(&testInvalidInt{}) + invalidIntCfg, err := config.New(&testInvalidInt{}, nil) require.NoError(t, err) - invalidFloatCfg, err := config.New(&testInvalidFloat{}) + invalidFloatCfg, err := config.New(&testInvalidFloat{}, nil) require.NoError(t, err) - invalidBoolCfg, err := config.New(&testInvalidBool{}) + invalidBoolCfg, err := config.New(&testInvalidBool{}, nil) require.NoError(t, err) - missingCfg, err := config.New(&testMissingValue{}) + missingCfg, err := config.New(&testMissingValue{}, nil) require.NoError(t, err) prmError, err := NewParam(config.SourceConsul, &testConsulGet{err: true}) require.NoError(t, err) - invalidFileIntCfg, err := config.New(&testInvalidFileInt{}) + invalidFileIntCfg, err := config.New(&testInvalidFileInt{}, nil) require.NoError(t, err) - fileNotExistCfg, err := config.New(&testFileDoesNotExist{}) + fileNotExistCfg, err := config.New(&testFileDoesNotExist{}, nil) require.NoError(t, err) type fields struct { From e14bc6b0b43675f2ac5cace8cf2fb96fe03d6fd0 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Wed, 30 Dec 2020 16:19:51 +0200 Subject: [PATCH 3/9] Notification addition --- go.mod | 4 ++-- vendor/modules.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index d841ae45..00e91320 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/beatlabs/harvester -go 1.13 +go 1.15 require ( - github.com/hashicorp/go-hclog v0.15.0 github.com/hashicorp/consul/api v1.8.1 + github.com/hashicorp/go-hclog v0.15.0 github.com/stretchr/testify v1.6.1 ) diff --git a/vendor/modules.txt b/vendor/modules.txt index 0eabd431..5035ac4f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -5,11 +5,13 @@ github.com/davecgh/go-spew/spew # github.com/fatih/color v1.9.0 github.com/fatih/color # github.com/hashicorp/consul/api v1.8.1 +## explicit github.com/hashicorp/consul/api github.com/hashicorp/consul/api/watch # github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cleanhttp # github.com/hashicorp/go-hclog v0.15.0 +## explicit github.com/hashicorp/go-hclog # github.com/hashicorp/go-immutable-radix v1.0.0 github.com/hashicorp/go-immutable-radix @@ -30,6 +32,7 @@ github.com/mitchellh/mapstructure # github.com/pmezard/go-difflib v1.0.0 github.com/pmezard/go-difflib/difflib # github.com/stretchr/testify v1.6.1 +## explicit github.com/stretchr/testify/assert github.com/stretchr/testify/require # golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae From cb488aa534ef2ed008e5240893b24bb09f2edc16 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Wed, 30 Dec 2020 17:50:11 +0200 Subject: [PATCH 4/9] Notification addition --- README.md | 4 + examples/06_notification/main.go | 2 +- harvester.go | 138 +++++++++++++++++++------------ harvester_test.go | 26 ++++++ 4 files changed, 117 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index c99095ad..849aaece 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,10 @@ The `Harvester` builder pattern is used to create a `Harvester` instance. The bu The above snippet set's up a `Harvester` instance with consul seed and monitor. +## Notification support + +In order to be able to monitor the changes in the configuration we can provide + ## Consul Consul has support for versioning (`ModifyIndex`) which allows us to change the value only if the version is higher than the one currently. diff --git a/examples/06_notification/main.go b/examples/06_notification/main.go index ae8bf2fe..f72a20e9 100644 --- a/examples/06_notification/main.go +++ b/examples/06_notification/main.go @@ -37,7 +37,7 @@ func main() { wg.Done() }() - h, err := harvester.NewWithNotification(&cfg, chNotify).Create() + h, err := harvester.New(&cfg).WithNotification(chNotify).Create() if err != nil { log.Fatalf("failed to create harvester: %v", err) } diff --git a/harvester.go b/harvester.go index ee928e3f..3947af6b 100644 --- a/harvester.go +++ b/harvester.go @@ -2,6 +2,7 @@ package harvester import ( "context" + "errors" "time" "github.com/beatlabs/harvester/config" @@ -45,37 +46,35 @@ func (h *harvester) Harvest(ctx context.Context) error { return h.monitor.Monitor(ctx) } +type consulConfig struct { + addr, dataCenter, token string + timeout time.Duration +} + // Builder of a harvester instance. type Builder struct { - cfg *config.Config - watchers []monitor.Watcher - seedParams []seed.Param - err error + cfg interface{} + seedConsulCfg *consulConfig + monitorConsulCfg *consulConfig + err error + chNotify chan<- string } // New constructor. func New(cfg interface{}) *Builder { - b := &Builder{} - c, err := config.New(cfg, nil) - if err != nil { - b.err = err - return b - } - b.cfg = c - b.seedParams = []seed.Param{} - return b + return &Builder{cfg: cfg} } -// NewWithNotification constructor. -func NewWithNotification(cfg interface{}, chNotify chan string) *Builder { - b := &Builder{} - c, err := config.New(cfg, chNotify) - if err != nil { - b.err = err +// WithNotification constructor. +func (b *Builder) WithNotification(chNotify chan<- string) *Builder { + if b.err != nil { return b } - b.cfg = c - b.seedParams = []seed.Param{} + if chNotify == nil { + b.err = errors.New("notification channel is nil") + return b + } + b.chNotify = chNotify return b } @@ -84,41 +83,27 @@ func (b *Builder) WithConsulSeed(addr, dataCenter, token string, timeout time.Du if b.err != nil { return b } - getter, err := seedConsul.New(addr, dataCenter, token, timeout) - if err != nil { - b.err = err - return b - } - p, err := seed.NewParam(config.SourceConsul, getter) - if err != nil { - b.err = err - return b + b.seedConsulCfg = &consulConfig{ + addr: addr, + dataCenter: dataCenter, + token: token, + timeout: timeout, } - b.seedParams = append(b.seedParams, *p) return b } // WithConsulMonitor enables support for monitoring key/prefixes on ConsulLogger. It automatically parses the config // and monitors every field found tagged with ConsulLogger. -func (b *Builder) WithConsulMonitor(addr, dc, token string, timeout time.Duration) *Builder { +func (b *Builder) WithConsulMonitor(addr, dataCenter, token string, timeout time.Duration) *Builder { if b.err != nil { return b } - items := make([]consul.Item, 0) - for _, field := range b.cfg.Fields { - consulKey, ok := field.Sources()[config.SourceConsul] - if !ok { - continue - } - log.Infof(`automatically monitoring consul key "%s"`, consulKey) - items = append(items, consul.NewKeyItem(consulKey)) + b.monitorConsulCfg = &consulConfig{ + addr: addr, + dataCenter: dataCenter, + token: token, + timeout: timeout, } - wtc, err := consul.New(addr, dc, token, timeout, items...) - if err != nil { - b.err = err - return b - } - b.watchers = append(b.watchers, wtc) return b } @@ -127,15 +112,64 @@ func (b *Builder) Create() (Harvester, error) { if b.err != nil { return nil, b.err } - sd := seed.New(b.seedParams...) - var mon Monitor - if len(b.watchers) == 0 { - return &harvester{seeder: sd, cfg: b.cfg}, nil + cfg, err := config.New(b.cfg, b.chNotify) + if err != nil { + return nil, err + } + + sd, err := b.setupSeeding() + if err != nil { + return nil, err + } + mon, err := b.setupMonitoring(cfg) + if err != nil { + return nil, err } - mon, err := monitor.New(b.cfg, b.watchers...) + + return &harvester{seeder: sd, monitor: mon, cfg: cfg}, nil +} + +func (b *Builder) setupSeeding() (Seeder, error) { + pp := make([]seed.Param, 0) + if b.seedConsulCfg != nil { + + getter, err := seedConsul.New(b.seedConsulCfg.addr, b.seedConsulCfg.dataCenter, b.seedConsulCfg.token, b.seedConsulCfg.timeout) + if err != nil { + return nil, err + } + + p, err := seed.NewParam(config.SourceConsul, getter) + if err != nil { + return nil, err + } + pp = append(pp, *p) + } + + return seed.New(pp...), nil +} + +func (b *Builder) setupMonitoring(cfg *config.Config) (Monitor, error) { + if b.monitorConsulCfg == nil { + return nil, nil + } + items := make([]consul.Item, 0) + for _, field := range cfg.Fields { + consulKey, ok := field.Sources()[config.SourceConsul] + if !ok { + continue + } + log.Infof(`automatically monitoring consul key "%s"`, consulKey) + items = append(items, consul.NewKeyItem(consulKey)) + } + wtc, err := consul.New(b.monitorConsulCfg.addr, b.monitorConsulCfg.dataCenter, b.monitorConsulCfg.token, b.monitorConsulCfg.timeout, items...) + if err != nil { + return nil, err + } + + mon, err := monitor.New(cfg, wtc) if err != nil { return nil, err } - return &harvester{seeder: sd, monitor: mon, cfg: b.cfg}, nil + return mon, nil } diff --git a/harvester_test.go b/harvester_test.go index d2f6f8ed..63a38385 100644 --- a/harvester_test.go +++ b/harvester_test.go @@ -43,6 +43,32 @@ func TestCreateWithConsul(t *testing.T) { } } +func TestWithNotification(t *testing.T) { + type args struct { + cfg interface{} + chNotify chan<- string + } + tests := map[string]struct { + args args + wantErr bool + }{ + "nil notify channel": {args: args{cfg: &testConfig{}, chNotify: nil}, wantErr: true}, + "success": {args: args{cfg: &testConfig{}, chNotify: make(chan string, 0)}, wantErr: false}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got, err := New(tt.args.cfg).WithNotification(tt.args.chNotify).Create() + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + } + }) + } +} + func TestCreate_NoConsul(t *testing.T) { cfg := &testConfigNoConsul{} got, err := New(cfg).Create() From cad4f5f7a808caf5713d222a4aa6da866193f5e0 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Wed, 30 Dec 2020 17:50:54 +0200 Subject: [PATCH 5/9] Notification addition --- harvester_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harvester_test.go b/harvester_test.go index 63a38385..a7fa434c 100644 --- a/harvester_test.go +++ b/harvester_test.go @@ -53,7 +53,7 @@ func TestWithNotification(t *testing.T) { wantErr bool }{ "nil notify channel": {args: args{cfg: &testConfig{}, chNotify: nil}, wantErr: true}, - "success": {args: args{cfg: &testConfig{}, chNotify: make(chan string, 0)}, wantErr: false}, + "success": {args: args{cfg: &testConfig{}, chNotify: make(chan string)}, wantErr: false}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { From 678f0008ad9dfd84a82ad4ee0c36da87d220dd8c Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Wed, 30 Dec 2020 18:05:41 +0200 Subject: [PATCH 6/9] Notification addition --- config/config_test.go | 44 ++++++++++++-------------- harvester_test.go | 13 ++++---- log/log_test.go | 17 +++++----- monitor/consul/watcher_test.go | 26 +++++++-------- monitor/monitor_test.go | 15 ++++----- seed/consul/getter_integration_test.go | 13 ++++---- seed/consul/getter_test.go | 13 ++++---- seed/seed_test.go | 36 ++++++++++----------- 8 files changed, 83 insertions(+), 94 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 911d99f2..a7778382 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -17,23 +17,22 @@ func TestField_Set(t *testing.T) { value string version uint64 } - tests := []struct { - name string + tests := map[string]struct { field Field args args wantErr bool }{ - {name: "success String", field: *cfg.Fields[0], args: args{value: "John Doe", version: 3}, wantErr: false}, - {name: "success Int64", field: *cfg.Fields[1], args: args{value: "18", version: 1}, wantErr: false}, - {name: "success Float64", field: *cfg.Fields[2], args: args{value: "99.9", version: 1}, wantErr: false}, - {name: "success Bool", field: *cfg.Fields[3], args: args{value: "true", version: 1}, wantErr: false}, - {name: "failure Int64", field: *cfg.Fields[1], args: args{value: "XXX", version: 1}, wantErr: true}, - {name: "failure Float64", field: *cfg.Fields[2], args: args{value: "XXX", version: 1}, wantErr: true}, - {name: "failure Bool", field: *cfg.Fields[3], args: args{value: "XXX", version: 1}, wantErr: true}, - {name: "warn String version older", field: *cfg.Fields[0], args: args{value: "John Doe", version: 2}, wantErr: false}, + "success String": {field: *cfg.Fields[0], args: args{value: "John Doe", version: 3}, wantErr: false}, + "success Int64": {field: *cfg.Fields[1], args: args{value: "18", version: 1}, wantErr: false}, + "success Float64": {field: *cfg.Fields[2], args: args{value: "99.9", version: 1}, wantErr: false}, + "success Bool": {field: *cfg.Fields[3], args: args{value: "true", version: 1}, wantErr: false}, + "failure Int64": {field: *cfg.Fields[1], args: args{value: "XXX", version: 1}, wantErr: true}, + "failure Float64": {field: *cfg.Fields[2], args: args{value: "XXX", version: 1}, wantErr: true}, + "failure Bool": {field: *cfg.Fields[3], args: args{value: "XXX", version: 1}, wantErr: true}, + "warn String version older": {field: *cfg.Fields[0], args: args{value: "John Doe", version: 2}, wantErr: false}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { err := tt.field.Set(tt.args.value, tt.args.version) if tt.wantErr { assert.Error(t, err) @@ -48,21 +47,20 @@ func TestNew(t *testing.T) { type args struct { cfg interface{} } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{cfg: &testConfig{}}, wantErr: false}, - {name: "cfg is nil", args: args{cfg: nil}, wantErr: true}, - {name: "cfg is not pointer", args: args{cfg: testConfig{}}, wantErr: true}, - {name: "cfg field not supported", args: args{cfg: &testInvalidTypeConfig{}}, wantErr: true}, - {name: "cfg duplicate consul key", args: args{cfg: &testDuplicateConfig{}}, wantErr: true}, - {name: "cfg tagged struct not supported", args: args{cfg: &testInvalidNestedStructWithTags{}}, wantErr: true}, - {name: "cfg nested duplicate consul key", args: args{cfg: &testDuplicateNestedConsulConfig{}}, wantErr: true}, + "success": {args: args{cfg: &testConfig{}}, wantErr: false}, + "cfg is nil": {args: args{cfg: nil}, wantErr: true}, + "cfg is not pointer": {args: args{cfg: testConfig{}}, wantErr: true}, + "cfg field not supported": {args: args{cfg: &testInvalidTypeConfig{}}, wantErr: true}, + "cfg duplicate consul key": {args: args{cfg: &testDuplicateConfig{}}, wantErr: true}, + "cfg tagged struct not supported": {args: args{cfg: &testInvalidNestedStructWithTags{}}, wantErr: true}, + "cfg nested duplicate consul key": {args: args{cfg: &testDuplicateNestedConsulConfig{}}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.cfg, nil) if tt.wantErr { assert.Error(t, err) diff --git a/harvester_test.go b/harvester_test.go index a7fa434c..1690691f 100644 --- a/harvester_test.go +++ b/harvester_test.go @@ -17,17 +17,16 @@ func TestCreateWithConsul(t *testing.T) { cfg interface{} addr string } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "invalid cfg", args: args{cfg: "test", addr: addr}, wantErr: true}, - {name: "invalid address", args: args{cfg: &testConfig{}, addr: ""}, wantErr: true}, - {name: "success", args: args{cfg: &testConfig{}, addr: addr}, wantErr: false}, + "invalid cfg": {args: args{cfg: "test", addr: addr}, wantErr: true}, + "invalid address": {args: args{cfg: &testConfig{}, addr: ""}, wantErr: true}, + "success": {args: args{cfg: &testConfig{}, addr: addr}, wantErr: false}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.cfg). WithConsulSeed(tt.args.addr, "", "", 0). WithConsulMonitor(tt.args.addr, "", "", 0). diff --git a/log/log_test.go b/log/log_test.go index c6c79904..51b23834 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -32,19 +32,18 @@ func TestSetupLogging(t *testing.T) { errorf Func debugf Func } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: false}, - {name: "missing info", args: args{infof: nil, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, - {name: "missing warn", args: args{infof: stubLogf, warnf: nil, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, - {name: "missing error", args: args{infof: stubLogf, warnf: stubLogf, errorf: nil, debugf: stubLogf}, wantErr: true}, - {name: "missing debug", args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf}, wantErr: true}, + "success": {args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: false}, + "missing info": {args: args{infof: nil, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, + "missing warn": {args: args{infof: stubLogf, warnf: nil, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, + "missing error": {args: args{infof: stubLogf, warnf: stubLogf, errorf: nil, debugf: stubLogf}, wantErr: true}, + "missing debug": {args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { err := Setup(tt.args.infof, tt.args.warnf, tt.args.errorf, tt.args.debugf) if tt.wantErr { assert.Error(t, err) diff --git a/monitor/consul/watcher_test.go b/monitor/consul/watcher_test.go index 6fe3aab8..ff2a1125 100644 --- a/monitor/consul/watcher_test.go +++ b/monitor/consul/watcher_test.go @@ -17,18 +17,17 @@ func TestNew(t *testing.T) { timeout time.Duration ii []Item } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{addr: "xxx", timeout: 1 * time.Second, ii: ii}, wantErr: false}, - {name: "success default timeout", args: args{addr: "xxx", timeout: 0, ii: ii}, wantErr: false}, - {name: "empty address", args: args{addr: "", timeout: 1 * time.Second, ii: ii}, wantErr: true}, - {name: "empty items", args: args{addr: "xxx", timeout: 1 * time.Second, ii: nil}, wantErr: true}, + "success": {args: args{addr: "xxx", timeout: 1 * time.Second, ii: ii}, wantErr: false}, + "success default timeout": {args: args{addr: "xxx", timeout: 0, ii: ii}, wantErr: false}, + "empty address": {args: args{addr: "", timeout: 1 * time.Second, ii: ii}, wantErr: true}, + "empty items": {args: args{addr: "xxx", timeout: 1 * time.Second, ii: nil}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.addr, "dc", "token", tt.args.timeout, tt.args.ii...) if tt.wantErr { assert.Error(t, err) @@ -48,16 +47,15 @@ func TestWatcher_Watch(t *testing.T) { ctx context.Context ch chan<- []*change.Change } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "missing context", args: args{}, wantErr: true}, - {name: "missing chan", args: args{ctx: context.Background()}, wantErr: true}, + "missing context": {args: args{}, wantErr: true}, + "missing chan": {args: args{ctx: context.Background()}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { err = w.Watch(tt.args.ctx, tt.args.ch) if tt.wantErr { assert.Error(t, err) diff --git a/monitor/monitor_test.go b/monitor/monitor_test.go index 91c40436..e60a7c22 100644 --- a/monitor/monitor_test.go +++ b/monitor/monitor_test.go @@ -25,18 +25,17 @@ func TestNew(t *testing.T) { cfg *config.Config ww []Watcher } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{cfg: cfg, ww: watchers}, wantErr: false}, - {name: "missing cfg", args: args{cfg: nil, ww: watchers}, wantErr: true}, - {name: "empty watchers", args: args{cfg: cfg, ww: nil}, wantErr: true}, - {name: "error watchers", args: args{cfg: errCfg, ww: watchers}, wantErr: true}, + "success": {args: args{cfg: cfg, ww: watchers}, wantErr: false}, + "missing cfg": {args: args{cfg: nil, ww: watchers}, wantErr: true}, + "empty watchers": {args: args{cfg: cfg, ww: nil}, wantErr: true}, + "error watchers": {args: args{cfg: errCfg, ww: watchers}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.cfg, tt.args.ww...) if tt.wantErr { assert.Error(t, err) diff --git a/seed/consul/getter_integration_test.go b/seed/consul/getter_integration_test.go index 108b7c5b..197d72b5 100644 --- a/seed/consul/getter_integration_test.go +++ b/seed/consul/getter_integration_test.go @@ -45,18 +45,17 @@ func TestGetter_Get(t *testing.T) { key string addr string } - tests := []struct { - name string + tests := map[string]struct { args args want *string wantErr bool }{ - {name: "success", args: args{addr: addr, key: "get_key1"}, want: &one, wantErr: false}, - {name: "missing key", args: args{addr: addr, key: "get_key2"}, want: nil, wantErr: false}, - {name: "wrong address", args: args{addr: "xxx", key: "get_key1"}, want: nil, wantErr: true}, + "success": {args: args{addr: addr, key: "get_key1"}, want: &one, wantErr: false}, + "missing key": {args: args{addr: addr, key: "get_key2"}, want: nil, wantErr: false}, + "wrong address": {args: args{addr: "xxx", key: "get_key1"}, want: nil, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { gtr, err := New(tt.args.addr, "", "", 0) require.NoError(t, err) got, version, err := gtr.Get(tt.args.key) diff --git a/seed/consul/getter_test.go b/seed/consul/getter_test.go index ee057153..a1847252 100644 --- a/seed/consul/getter_test.go +++ b/seed/consul/getter_test.go @@ -12,17 +12,16 @@ func TestNew(t *testing.T) { addr string timeout time.Duration } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{addr: "addr", timeout: 0}, wantErr: false}, - {name: "success explicit timeout", args: args{addr: "addr", timeout: 30 * time.Second}, wantErr: false}, - {name: "missing address", args: args{addr: ""}, wantErr: true}, + "success": {args: args{addr: "addr", timeout: 0}, wantErr: false}, + "success explicit timeout": {args: args{addr: "addr", timeout: 30 * time.Second}, wantErr: false}, + "missing address": {args: args{addr: ""}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.addr, "dc", "token", 0) if tt.wantErr { assert.Error(t, err) diff --git a/seed/seed_test.go b/seed/seed_test.go index d0933608..89b9219d 100644 --- a/seed/seed_test.go +++ b/seed/seed_test.go @@ -17,16 +17,15 @@ func TestNewParam(t *testing.T) { src config.Source getter Getter } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{src: config.SourceConsul, getter: &testConsulGet{}}, wantErr: false}, - {name: "missing getter", args: args{src: config.SourceConsul, getter: nil}, wantErr: true}, + "success": {args: args{src: config.SourceConsul, getter: &testConsulGet{}}, wantErr: false}, + "missing getter": {args: args{src: config.SourceConsul, getter: nil}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := NewParam(tt.args.src, tt.args.getter) if tt.wantErr { assert.Error(t, err) @@ -169,24 +168,23 @@ func TestSeeder_Seed(t *testing.T) { type args struct { cfg *config.Config } - tests := []struct { - name string + tests := map[string]struct { fields fields args args wantErr bool }{ - {name: "success", fields: fields{consulParam: prmSuccess}, args: args{cfg: goodCfg}}, - {name: "consul get nil", args: args{cfg: goodCfg}, wantErr: true}, - {name: "consul get error, seed successful", fields: fields{consulParam: prmError}, args: args{cfg: goodCfg}}, - {name: "consul missing value", fields: fields{consulParam: prmSuccess}, args: args{cfg: missingCfg}, wantErr: true}, - {name: "invalid int", args: args{cfg: invalidIntCfg}, wantErr: true}, - {name: "invalid float", args: args{cfg: invalidFloatCfg}, wantErr: true}, - {name: "invalid bool", fields: fields{consulParam: prmSuccess}, args: args{cfg: invalidBoolCfg}, wantErr: true}, - {name: "invalid file int", args: args{cfg: invalidFileIntCfg}, wantErr: true}, - {name: "file read error, seed successful", args: args{cfg: fileNotExistCfg}}, + "success": {fields: fields{consulParam: prmSuccess}, args: args{cfg: goodCfg}}, + "consul get nil": {args: args{cfg: goodCfg}, wantErr: true}, + "consul get error, seed successful": {fields: fields{consulParam: prmError}, args: args{cfg: goodCfg}}, + "consul missing value": {fields: fields{consulParam: prmSuccess}, args: args{cfg: missingCfg}, wantErr: true}, + "invalid int": {args: args{cfg: invalidIntCfg}, wantErr: true}, + "invalid float": {args: args{cfg: invalidFloatCfg}, wantErr: true}, + "invalid bool": {fields: fields{consulParam: prmSuccess}, args: args{cfg: invalidBoolCfg}, wantErr: true}, + "invalid file int": {args: args{cfg: invalidFileIntCfg}, wantErr: true}, + "file read error, seed successful": {args: args{cfg: fileNotExistCfg}}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { var s *Seeder if tt.fields.consulParam == nil { s = New() From 2cb748a8a15e457dd64453fa749f38e05a6eaab7 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Thu, 31 Dec 2020 14:09:01 +0200 Subject: [PATCH 7/9] Notification addition --- Makefile | 1 + README.md | 7 ++++++- config/config.go | 24 ++++++++++++++++++++---- config/config_test.go | 20 +++++++++++++------- config/parser.go | 6 +++--- examples/06_notification/main.go | 9 +++++---- harvester.go | 4 ++-- harvester_test.go | 5 +++-- 8 files changed, 53 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 678546ec..e3ff54ea 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ deeplint: fmtcheck deps: docker container inspect badger > /dev/null 2>&1 || docker run -d --rm -p 8500:8500 -p 8600:8600/udp --name=badger consul:1.4.3 agent -server -ui -node=server-1 -bootstrap-expect=1 -client=0.0.0.0 -http-port 8500 -log-level=err + sleep 1 ci: fmtcheck lint deps go test ./... -race -cover -tags=integration -coverprofile=coverage.txt -covermode=atomic diff --git a/README.md b/README.md index 849aaece..7ddcf78a 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,12 @@ The above snippet set's up a `Harvester` instance with consul seed and monitor. ## Notification support -In order to be able to monitor the changes in the configuration we can provide +In order to be able to monitor the changes in the configuration we provide a way to notify when a change is happening via the builder. + +```go + h, err := harvester.New(&cfg).WithNotification(chNotify).Create() + ... +``` ## Consul diff --git a/config/config.go b/config/config.go index 699e25a1..00607f48 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,17 @@ type CfgType interface { SetString(string) error } +type ChangeNotification struct { + Name string + Type string + Previous string + Current string +} + +func (n ChangeNotification) String() string { + return fmt.Sprintf("field [%s] of type [%s] changed from [%s] to [%s]", n.Name, n.Type, n.Previous, n.Current) +} + // Field definition of a config value that can change. type Field struct { name string @@ -40,11 +51,11 @@ type Field struct { version uint64 structField CfgType sources map[Source]string - chNotify chan<- string + chNotify chan<- ChangeNotification } // newField constructor. -func newField(prefix string, fld reflect.StructField, val reflect.Value, chNotify chan<- string) *Field { +func newField(prefix string, fld reflect.StructField, val reflect.Value, chNotify chan<- ChangeNotification) *Field { f := &Field{ name: prefix + fld.Name, tp: fld.Type.Name(), @@ -112,7 +123,12 @@ func (f *Field) sendNotification(prev string, current string) { if f.chNotify == nil { return } - f.chNotify <- fmt.Sprintf("field [%s] of type [%s] changed from [%s] to [%s]", f.name, f.tp, prev, current) + f.chNotify <- ChangeNotification{ + Name: f.name, + Type: f.tp, + Previous: prev, + Current: current, + } } // Config manages configuration and handles updates on the values. @@ -121,7 +137,7 @@ type Config struct { } // New creates a new monitor. -func New(cfg interface{}, chNotify chan<- string) (*Config, error) { +func New(cfg interface{}, chNotify chan<- ChangeNotification) (*Config, error) { if cfg == nil { return nil, errors.New("configuration is nil") } diff --git a/config/config_test.go b/config/config_test.go index a7778382..d5988782 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -95,27 +95,33 @@ func assertField(t *testing.T, fld *Field, name, typ string, sources map[Source] func TestConfig_Set(t *testing.T) { c := testConfig{} - chNotify := make(chan string, 1) + chNotify := make(chan ChangeNotification, 1) cfg, err := New(&c, chNotify) require.NoError(t, err) err = cfg.Fields[0].Set("John Doe", 1) assert.NoError(t, err) - assert.Equal(t, "field [Name] of type [String] changed from [] to [John Doe]", <-chNotify) + change := <-chNotify + assert.Equal(t, "field [Name] of type [String] changed from [] to [John Doe]", change.String()) err = cfg.Fields[1].Set("18", 1) assert.NoError(t, err) - assert.Equal(t, "field [Age] of type [Int64] changed from [0] to [18]", <-chNotify) + change = <-chNotify + assert.Equal(t, "field [Age] of type [Int64] changed from [0] to [18]", change.String()) err = cfg.Fields[2].Set("99.9", 1) assert.NoError(t, err) - assert.Equal(t, "field [Balance] of type [Float64] changed from [0.000000] to [99.9]", <-chNotify) + change = <-chNotify + assert.Equal(t, "field [Balance] of type [Float64] changed from [0.000000] to [99.9]", change.String()) err = cfg.Fields[3].Set("true", 1) assert.NoError(t, err) - assert.Equal(t, "field [HasJob] of type [Bool] changed from [false] to [true]", <-chNotify) + change = <-chNotify + assert.Equal(t, "field [HasJob] of type [Bool] changed from [false] to [true]", change.String()) err = cfg.Fields[4].Set("6000", 1) assert.NoError(t, err) - assert.Equal(t, "field [PositionSalary] of type [Int64] changed from [0] to [6000]", <-chNotify) + change = <-chNotify + assert.Equal(t, "field [PositionSalary] of type [Int64] changed from [0] to [6000]", change.String()) err = cfg.Fields[5].Set("baz", 1) assert.NoError(t, err) - assert.Equal(t, "field [LevelOneLevelTwoDeepField] of type [String] changed from [] to [baz]", <-chNotify) + change = <-chNotify + assert.Equal(t, "field [LevelOneLevelTwoDeepField] of type [String] changed from [] to [baz]", change.String()) assert.Equal(t, "John Doe", c.Name.Get()) assert.Equal(t, int64(18), c.Age.Get()) assert.Equal(t, 99.9, c.Balance.Get()) diff --git a/config/parser.go b/config/parser.go index 76128486..813db11d 100644 --- a/config/parser.go +++ b/config/parser.go @@ -22,7 +22,7 @@ func newParser() *parser { return &parser{} } -func (p *parser) ParseCfg(cfg interface{}, chNotify chan<- string) ([]*Field, error) { +func (p *parser) ParseCfg(cfg interface{}, chNotify chan<- ChangeNotification) ([]*Field, error) { p.dups = make(map[Source]string) tp := reflect.TypeOf(cfg) @@ -33,7 +33,7 @@ func (p *parser) ParseCfg(cfg interface{}, chNotify chan<- string) ([]*Field, er return p.getFields("", tp.Elem(), reflect.ValueOf(cfg).Elem(), chNotify) } -func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value, chNotify chan<- string) ([]*Field, error) { +func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value, chNotify chan<- ChangeNotification) ([]*Field, error) { var ff []*Field for i := 0; i < tp.NumField(); i++ { @@ -62,7 +62,7 @@ func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value, ch return ff, nil } -func (p *parser) createField(prefix string, f reflect.StructField, val reflect.Value, chNotify chan<- string) (*Field, error) { +func (p *parser) createField(prefix string, f reflect.StructField, val reflect.Value, chNotify chan<- ChangeNotification) (*Field, error) { fld := newField(prefix, f, val, chNotify) value, ok := fld.Sources()[SourceConsul] diff --git a/examples/06_notification/main.go b/examples/06_notification/main.go index f72a20e9..cde07a5d 100644 --- a/examples/06_notification/main.go +++ b/examples/06_notification/main.go @@ -7,10 +7,11 @@ import ( "sync" "github.com/beatlabs/harvester" + "github.com/beatlabs/harvester/config" harvestersync "github.com/beatlabs/harvester/sync" ) -type config struct { +type cfg struct { IndexName harvestersync.String `seed:"customers-v1"` CacheRetention harvestersync.Int64 `seed:"43200" env:"ENV_CACHE_RETENTION_SECONDS"` LogLevel harvestersync.String `seed:"DEBUG" flag:"loglevel"` @@ -25,14 +26,14 @@ func main() { log.Fatalf("failed to set env var: %v", err) } - cfg := config{} - chNotify := make(chan string) + cfg := cfg{} + chNotify := make(chan config.ChangeNotification) wg := sync.WaitGroup{} wg.Add(1) go func() { for change := range chNotify { - log.Printf("notification: " + change) + log.Printf("notification: " + change.String()) } wg.Done() }() diff --git a/harvester.go b/harvester.go index 3947af6b..3f97ffea 100644 --- a/harvester.go +++ b/harvester.go @@ -57,7 +57,7 @@ type Builder struct { seedConsulCfg *consulConfig monitorConsulCfg *consulConfig err error - chNotify chan<- string + chNotify chan<- config.ChangeNotification } // New constructor. @@ -66,7 +66,7 @@ func New(cfg interface{}) *Builder { } // WithNotification constructor. -func (b *Builder) WithNotification(chNotify chan<- string) *Builder { +func (b *Builder) WithNotification(chNotify chan<- config.ChangeNotification) *Builder { if b.err != nil { return b } diff --git a/harvester_test.go b/harvester_test.go index 1690691f..2381d252 100644 --- a/harvester_test.go +++ b/harvester_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/beatlabs/harvester/config" "github.com/beatlabs/harvester/sync" "github.com/stretchr/testify/assert" ) @@ -45,14 +46,14 @@ func TestCreateWithConsul(t *testing.T) { func TestWithNotification(t *testing.T) { type args struct { cfg interface{} - chNotify chan<- string + chNotify chan<- config.ChangeNotification } tests := map[string]struct { args args wantErr bool }{ "nil notify channel": {args: args{cfg: &testConfig{}, chNotify: nil}, wantErr: true}, - "success": {args: args{cfg: &testConfig{}, chNotify: make(chan string)}, wantErr: false}, + "success": {args: args{cfg: &testConfig{}, chNotify: make(chan config.ChangeNotification)}, wantErr: false}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { From 73861e1363a81ccefb4e3f3d245b736656157e2f Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Thu, 31 Dec 2020 14:11:33 +0200 Subject: [PATCH 8/9] Notification addition --- config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.go b/config/config.go index 00607f48..2d633ec4 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,7 @@ type CfgType interface { SetString(string) error } +// ChangeNotification definition for a configuration change. type ChangeNotification struct { Name string Type string From 6793ed9ed4c0b55c7ce0f608e2a9c4bb0e39c225 Mon Sep 17 00:00:00 2001 From: Sotirios Mantziaris Date: Thu, 31 Dec 2020 15:39:02 +0200 Subject: [PATCH 9/9] Notification addition --- Makefile | 1 - seed/seed_test.go | 159 ++++++++++++++++++++++++++++------------------ 2 files changed, 98 insertions(+), 62 deletions(-) diff --git a/Makefile b/Makefile index e3ff54ea..678546ec 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,6 @@ deeplint: fmtcheck deps: docker container inspect badger > /dev/null 2>&1 || docker run -d --rm -p 8500:8500 -p 8600:8600/udp --name=badger consul:1.4.3 agent -server -ui -node=server-1 -bootstrap-expect=1 -client=0.0.0.0 -http-port 8500 -log-level=err - sleep 1 ci: fmtcheck lint deps go test ./... -race -cover -tags=integration -coverprofile=coverage.txt -covermode=atomic diff --git a/seed/seed_test.go b/seed/seed_test.go index 89b9219d..dfb2e5ee 100644 --- a/seed/seed_test.go +++ b/seed/seed_test.go @@ -138,74 +138,111 @@ func TestSeeder_Seed_Flags(t *testing.T) { } func TestSeeder_Seed(t *testing.T) { - require.NoError(t, os.Setenv("ENV_XXX", "XXX")) require.NoError(t, os.Setenv("ENV_AGE", "25")) require.NoError(t, os.Setenv("ENV_WORK_HOURS", "9h")) - c := testConfig{} - goodCfg, err := config.New(&c, nil) - require.NoError(t, err) - prmSuccess, err := NewParam(config.SourceConsul, &testConsulGet{}) - require.NoError(t, err) - invalidIntCfg, err := config.New(&testInvalidInt{}, nil) - require.NoError(t, err) - invalidFloatCfg, err := config.New(&testInvalidFloat{}, nil) - require.NoError(t, err) - invalidBoolCfg, err := config.New(&testInvalidBool{}, nil) - require.NoError(t, err) - missingCfg, err := config.New(&testMissingValue{}, nil) - require.NoError(t, err) prmError, err := NewParam(config.SourceConsul, &testConsulGet{err: true}) require.NoError(t, err) - invalidFileIntCfg, err := config.New(&testInvalidFileInt{}, nil) - require.NoError(t, err) - fileNotExistCfg, err := config.New(&testFileDoesNotExist{}, nil) - require.NoError(t, err) - type fields struct { - consulParam *Param - } - type args struct { - cfg *config.Config - } - tests := map[string]struct { - fields fields - args args - wantErr bool - }{ - "success": {fields: fields{consulParam: prmSuccess}, args: args{cfg: goodCfg}}, - "consul get nil": {args: args{cfg: goodCfg}, wantErr: true}, - "consul get error, seed successful": {fields: fields{consulParam: prmError}, args: args{cfg: goodCfg}}, - "consul missing value": {fields: fields{consulParam: prmSuccess}, args: args{cfg: missingCfg}, wantErr: true}, - "invalid int": {args: args{cfg: invalidIntCfg}, wantErr: true}, - "invalid float": {args: args{cfg: invalidFloatCfg}, wantErr: true}, - "invalid bool": {fields: fields{consulParam: prmSuccess}, args: args{cfg: invalidBoolCfg}, wantErr: true}, - "invalid file int": {args: args{cfg: invalidFileIntCfg}, wantErr: true}, - "file read error, seed successful": {args: args{cfg: fileNotExistCfg}}, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - var s *Seeder - if tt.fields.consulParam == nil { - s = New() - } else { - s = New(*tt.fields.consulParam) - } + t.Run("consul success", func(t *testing.T) { + c := testConfig{} + goodCfg, err := config.New(&c, nil) + require.NoError(t, err) + prmSuccess, err := NewParam(config.SourceConsul, &testConsulGet{}) + require.NoError(t, err) - err := s.Seed(tt.args.cfg) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, "John Doe", c.Name.Get()) - assert.Equal(t, int64(25), c.Age.Get()) - assert.Equal(t, 99.9, c.Balance.Get()) - assert.True(t, c.HasJob.Get()) - assert.Equal(t, "foobar", c.About.Get()) - assert.Equal(t, 9*time.Hour, c.WorkHours.Get()) - } - }) - } + err = New(*prmSuccess).Seed(goodCfg) + + assert.NoError(t, err) + assert.Equal(t, "John Doe", c.Name.Get()) + assert.Equal(t, int64(25), c.Age.Get()) + assert.Equal(t, 99.9, c.Balance.Get()) + assert.True(t, c.HasJob.Get()) + assert.Equal(t, "foobar", c.About.Get()) + assert.Equal(t, 9*time.Hour, c.WorkHours.Get()) + }) + + t.Run("consul error, success", func(t *testing.T) { + c := testConfig{} + goodCfg, err := config.New(&c, nil) + require.NoError(t, err) + + err = New(*prmError).Seed(goodCfg) + + assert.NoError(t, err) + assert.Equal(t, "John Doe", c.Name.Get()) + assert.Equal(t, int64(25), c.Age.Get()) + assert.Equal(t, 99.9, c.Balance.Get()) + assert.True(t, c.HasJob.Get()) + assert.Equal(t, "foobar", c.About.Get()) + assert.Equal(t, 9*time.Hour, c.WorkHours.Get()) + }) + + t.Run("file not exists, success", func(t *testing.T) { + c := &testFileDoesNotExist{} + fileNotExistCfg, err := config.New(c, nil) + require.NoError(t, err) + + err = New(*prmError).Seed(fileNotExistCfg) + + assert.NoError(t, err) + assert.Equal(t, int64(20), c.Age.Get()) + }) + + t.Run("consul nil, failure", func(t *testing.T) { + c := testConfig{} + goodCfg, err := config.New(&c, nil) + require.NoError(t, err) + + err = New().Seed(goodCfg) + + assert.Error(t, err) + }) + + t.Run("consul missing value, failure", func(t *testing.T) { + missingCfg, err := config.New(&testMissingValue{}, nil) + require.NoError(t, err) + + err = New().Seed(missingCfg) + + assert.Error(t, err) + }) + + t.Run("invalid int, failure", func(t *testing.T) { + invalidIntCfg, err := config.New(&testInvalidInt{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidIntCfg) + + assert.Error(t, err) + }) + + t.Run("invalid float, failure", func(t *testing.T) { + invalidFloatCfg, err := config.New(&testInvalidFloat{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidFloatCfg) + + assert.Error(t, err) + }) + + t.Run("invalid bool, failure", func(t *testing.T) { + invalidBoolCfg, err := config.New(&testInvalidBool{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidBoolCfg) + + assert.Error(t, err) + }) + + t.Run("invalid file int, failure", func(t *testing.T) { + invalidFileIntCfg, err := config.New(&testInvalidFileInt{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidFileIntCfg) + + assert.Error(t, err) + }) } type testConfig struct {