mirror of
https://github.com/spf13/viper
synced 2025-05-15 08:37:19 +00:00
Merge 3424c8c181
into 727a41c38a
This commit is contained in:
commit
1bf9cd648a
3 changed files with 198 additions and 74 deletions
|
@ -176,11 +176,15 @@ Optionally you can provide a function for Viper to run each time a change occurs
|
||||||
|
|
||||||
```go
|
```go
|
||||||
viper.WatchConfig()
|
viper.WatchConfig()
|
||||||
|
defer CancelWatchConfig()
|
||||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
fmt.Println("Config file changed:", e.Name)
|
fmt.Println("Config file changed:", e.Name)
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you wish to stop watching the configPaths, simply call viper.CancelWatchConfig().
|
||||||
|
Note: This might be necessary if your tests involve trying out various config files.
|
||||||
|
|
||||||
### Reading Config from io.Reader
|
### Reading Config from io.Reader
|
||||||
|
|
||||||
Viper predefines many configuration sources such as files, environment
|
Viper predefines many configuration sources such as files, environment
|
||||||
|
|
78
viper.go
78
viper.go
|
@ -217,6 +217,7 @@ type Viper struct {
|
||||||
properties *properties.Properties
|
properties *properties.Properties
|
||||||
|
|
||||||
onConfigChange func(fsnotify.Event)
|
onConfigChange func(fsnotify.Event)
|
||||||
|
cancelWatchConfig func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns an initialized Viper instance.
|
// New returns an initialized Viper instance.
|
||||||
|
@ -333,27 +334,33 @@ var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props
|
||||||
// SupportedRemoteProviders are universally supported remote providers.
|
// SupportedRemoteProviders are universally supported remote providers.
|
||||||
var SupportedRemoteProviders = []string{"etcd", "consul", "firestore"}
|
var SupportedRemoteProviders = []string{"etcd", "consul", "firestore"}
|
||||||
|
|
||||||
|
// OnConfigChange is used to set the handler to run for catching the event
|
||||||
|
// when the configFile have been externally modified by the filesystem.
|
||||||
func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) }
|
func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) }
|
||||||
func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
|
func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
|
||||||
v.onConfigChange = run
|
v.onConfigChange = run
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchConfig() { v.WatchConfig() }
|
// CancelWatchConfig finishes the current configFile watch if it runs.
|
||||||
|
// When this function returns the configFile changes are not more looked after.
|
||||||
func (v *Viper) WatchConfig() {
|
func CancelWatchConfig() { v.cancelWatchConfig() }
|
||||||
initWG := sync.WaitGroup{}
|
func (v *Viper) CancelWatchConfig() {
|
||||||
initWG.Add(1)
|
if v.cancelWatchConfig != nil {
|
||||||
go func() {
|
v.cancelWatchConfig()
|
||||||
watcher, err := fsnotify.NewWatcher()
|
v.cancelWatchConfig = nil
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
defer watcher.Close()
|
}
|
||||||
|
|
||||||
|
// WatchConfig watches and notifies changes on the file configFile.
|
||||||
|
func WatchConfig() { v.WatchConfig() }
|
||||||
|
func (v *Viper) WatchConfig() {
|
||||||
|
// Make sure not to run twice this routine
|
||||||
|
v.CancelWatchConfig()
|
||||||
|
|
||||||
// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
|
// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
|
||||||
filename, err := v.getConfigFile()
|
filename, err := v.getConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
log.Printf("error: %v\n", err)
|
||||||
initWG.Done()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,16 +368,23 @@ func (v *Viper) WatchConfig() {
|
||||||
configDir, _ := filepath.Split(configFile)
|
configDir, _ := filepath.Split(configFile)
|
||||||
realConfigFile, _ := filepath.EvalSymlinks(filename)
|
realConfigFile, _ := filepath.EvalSymlinks(filename)
|
||||||
|
|
||||||
eventsWG := sync.WaitGroup{}
|
watcher, err := fsnotify.NewWatcher()
|
||||||
eventsWG.Add(1)
|
if err != nil {
|
||||||
go func() {
|
log.Fatal(err)
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok { // 'Events' channel is closed
|
|
||||||
eventsWG.Done()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
watcher.Add(configDir)
|
||||||
|
|
||||||
|
// cancellation and waiting point
|
||||||
|
var watcherGroup sync.WaitGroup
|
||||||
|
v.cancelWatchConfig = func() {
|
||||||
|
watcher.Close()
|
||||||
|
watcherGroup.Wait()
|
||||||
|
}
|
||||||
|
// Process watcher events
|
||||||
|
watcherGroup.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer watcherGroup.Done()
|
||||||
|
for event := range watcher.Events {
|
||||||
currentConfigFile, _ := filepath.EvalSymlinks(filename)
|
currentConfigFile, _ := filepath.EvalSymlinks(filename)
|
||||||
// we only care about the config file with the following cases:
|
// we only care about the config file with the following cases:
|
||||||
// 1 - if the config file was modified or created
|
// 1 - if the config file was modified or created
|
||||||
|
@ -389,24 +403,18 @@ func (v *Viper) WatchConfig() {
|
||||||
}
|
}
|
||||||
} else if filepath.Clean(event.Name) == configFile &&
|
} else if filepath.Clean(event.Name) == configFile &&
|
||||||
event.Op&fsnotify.Remove&fsnotify.Remove != 0 {
|
event.Op&fsnotify.Remove&fsnotify.Remove != 0 {
|
||||||
eventsWG.Done()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case err, ok := <-watcher.Errors:
|
}()
|
||||||
if ok { // 'Errors' channel is not closed
|
// Process watcher errors
|
||||||
|
watcherGroup.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer watcherGroup.Done()
|
||||||
|
for err := range watcher.Errors {
|
||||||
log.Printf("watcher error: %v\n", err)
|
log.Printf("watcher error: %v\n", err)
|
||||||
}
|
}
|
||||||
eventsWG.Done()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
watcher.Add(configDir)
|
|
||||||
initWG.Done() // done initializing the watch in this go routine, so the parent routine can move on...
|
|
||||||
eventsWG.Wait() // now, wait for event loop to end in this go-routine...
|
|
||||||
}()
|
|
||||||
initWG.Wait() // make sure that the go routine above fully ended before returning
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigFile explicitly defines the path, name and extension of the config file.
|
// SetConfigFile explicitly defines the path, name and extension of the config file.
|
||||||
|
@ -1499,6 +1507,10 @@ func (v *Viper) MergeConfigMap(cfg map[string]interface{}) error {
|
||||||
func WriteConfig() error { return v.WriteConfig() }
|
func WriteConfig() error { return v.WriteConfig() }
|
||||||
|
|
||||||
func (v *Viper) WriteConfig() error {
|
func (v *Viper) WriteConfig() error {
|
||||||
|
if v.cancelWatchConfig != nil {
|
||||||
|
v.cancelWatchConfig()
|
||||||
|
defer v.WatchConfig()
|
||||||
|
}
|
||||||
filename, err := v.getConfigFile()
|
filename, err := v.getConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
128
viper_test.go
128
viper_test.go
|
@ -18,7 +18,6 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -2148,17 +2147,15 @@ func TestWatchFile(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
_, err := os.Stat(configFile)
|
_, err := os.Stat(configFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Logf("test config file: %s\n", configFile)
|
done := make(chan struct{})
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(1)
|
|
||||||
v.OnConfigChange(func(in fsnotify.Event) {
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
t.Logf("config file changed")
|
t.Logf("config file changed")
|
||||||
wg.Done()
|
close(done)
|
||||||
})
|
})
|
||||||
v.WatchConfig()
|
v.WatchConfig()
|
||||||
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
||||||
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0640)
|
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0640)
|
||||||
wg.Wait()
|
<-done
|
||||||
// then the config value should have changed
|
// then the config value should have changed
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
assert.Equal(t, "baz", v.Get("foo"))
|
assert.Equal(t, "baz", v.Get("foo"))
|
||||||
|
@ -2171,13 +2168,12 @@ func TestWatchFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
v, watchDir, _, _ := newViperWithSymlinkedConfigFile(t)
|
v, watchDir, _, _ := newViperWithSymlinkedConfigFile(t)
|
||||||
// defer cleanup()
|
// defer cleanup()
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
v.WatchConfig()
|
v.WatchConfig()
|
||||||
|
done := make(chan struct{})
|
||||||
v.OnConfigChange(func(in fsnotify.Event) {
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
t.Logf("config file changed")
|
t.Logf("config file changed")
|
||||||
wg.Done()
|
close(done)
|
||||||
})
|
})
|
||||||
wg.Add(1)
|
|
||||||
// when link to another `config.yaml` file
|
// when link to another `config.yaml` file
|
||||||
dataDir2 := path.Join(watchDir, "data2")
|
dataDir2 := path.Join(watchDir, "data2")
|
||||||
err := os.Mkdir(dataDir2, 0777)
|
err := os.Mkdir(dataDir2, 0777)
|
||||||
|
@ -2188,11 +2184,123 @@ func TestWatchFile(t *testing.T) {
|
||||||
// change the symlink using the `ln -sfn` command
|
// change the symlink using the `ln -sfn` command
|
||||||
err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
|
err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
wg.Wait()
|
<-done
|
||||||
// then
|
// then
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
assert.Equal(t, "baz", v.Get("foo"))
|
assert.Equal(t, "baz", v.Get("foo"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("file content changed after cancel", func(t *testing.T) {
|
||||||
|
// given a `config.yaml` file being watched
|
||||||
|
v, configFile, cleanup := newViperWithConfigFile(t)
|
||||||
|
defer cleanup()
|
||||||
|
_, err := os.Stat(configFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// first run through with watching enabled
|
||||||
|
done := make(chan struct{})
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
done <- struct{}{}
|
||||||
|
})
|
||||||
|
v.WatchConfig()
|
||||||
|
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
||||||
|
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0640)
|
||||||
|
<-done
|
||||||
|
// then the config value should have changed
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "baz", v.Get("foo"))
|
||||||
|
|
||||||
|
// cancel and wait for the canceling to finish.
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
t.Error("CancelWatchConfig did not prevent second change from being seen.")
|
||||||
|
})
|
||||||
|
v.CancelWatchConfig()
|
||||||
|
|
||||||
|
// use another viper as a signal to wait this invisible write
|
||||||
|
v2 := New()
|
||||||
|
v2.SetConfigFile(configFile)
|
||||||
|
v2.WatchConfig()
|
||||||
|
v2.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
err = ioutil.WriteFile(configFile, []byte("foo: quz\n"), 0640)
|
||||||
|
<-done
|
||||||
|
// the config value should still be the same.
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "baz", v.Get("foo"), "CancelWatchConfig did not prevent second change from being seen.")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("do not watchConfig during writeConfig", func(t *testing.T) {
|
||||||
|
// given a `config.yaml` file being watched
|
||||||
|
v, configFile, cleanup := newViperWithConfigFile(t)
|
||||||
|
defer cleanup()
|
||||||
|
_, err := os.Stat(configFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// first run through with watching enabled
|
||||||
|
done := make(chan struct{})
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
v.WatchConfig()
|
||||||
|
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
||||||
|
err = ioutil.WriteFile(configFile, []byte("foo: baz\nbar: foo\n"), 0640)
|
||||||
|
<-done
|
||||||
|
// then the config value should have changed
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "baz", v.Get("foo"))
|
||||||
|
|
||||||
|
v.Set("foo", "bar")
|
||||||
|
v.WriteConfig()
|
||||||
|
v.Set("baz", "foo")
|
||||||
|
v.WriteConfig()
|
||||||
|
v.Set("aa", "bb")
|
||||||
|
v.WriteConfig()
|
||||||
|
v.Set("aaa", "10s")
|
||||||
|
v.WriteConfig()
|
||||||
|
|
||||||
|
f, err := ioutil.ReadFile(configFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string([]byte("aa: bb\naaa: 10s\nbar: foo\nbaz: foo\nfoo: bar\n")), string(f))
|
||||||
|
})
|
||||||
|
t.Run("still watchConfig after writeConfig", func(t *testing.T) {
|
||||||
|
// given a `config.yaml` file being watched
|
||||||
|
v, configFile, cleanup := newViperWithConfigFile(t)
|
||||||
|
defer cleanup()
|
||||||
|
_, err := os.Stat(configFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// first run through with watching enabled
|
||||||
|
done := make(chan struct{})
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
done <- struct{}{}
|
||||||
|
})
|
||||||
|
v.WatchConfig()
|
||||||
|
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
||||||
|
err = ioutil.WriteFile(configFile, []byte("foo: baz\nbar: foo\n"), 0640)
|
||||||
|
<-done
|
||||||
|
// then the config value should have changed
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "baz", v.Get("foo"))
|
||||||
|
|
||||||
|
v.Set("foo", "bar")
|
||||||
|
v.Set("baz", "foo")
|
||||||
|
v.Set("aa", "bb")
|
||||||
|
v.Set("aaa", "10s")
|
||||||
|
v.WriteConfig()
|
||||||
|
|
||||||
|
f, err := ioutil.ReadFile(configFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string([]byte("aa: bb\naaa: 10s\nbar: foo\nbaz: foo\nfoo: bar\n")), string(f))
|
||||||
|
// Check still watching after write
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
err = ioutil.WriteFile(configFile, []byte("foo: bar\n"), 0640)
|
||||||
|
require.Nil(t, err)
|
||||||
|
<-done // then the config value should have changed
|
||||||
|
assert.Equal(t, "bar", v.Get("foo"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
|
func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue