cdcb249449
* add config watcher to the config package * add logging to watcher * add test and refactor to add WatcherEvent. * add all API calls and fix a bug with recreated files * add tests for watcher * remove the unnecessary use of context * Add debug log and a test for file rename * use inode to detect if the file is recreated/replaced and only listen to create events. * tidy ups (#1535) * tidy ups * Add tests for inode reconcile * fix linux vs windows syscall * fix linux vs windows syscall * fix windows compile error * increase timeout * use ctime ID * remove remove/creation test as it's a use case that fail in linux * fix linux/windows to use Ino/CreationTime * fix the watcher to only overwrite current file id * fix linter error * fix remove/create test * set reconcile loop to 200 Milliseconds * fix watcher to not trigger event on remove, add more tests * on a remove event try to add the file back to the watcher and trigger the handler if success * fix race condition * fix flaky test * fix race conditions * set level to info * fix when file is removed and get an event for it after * fix to trigger handler when we get a remove but re-add fail * fix error message * add tests for directory watch and fixes * detect if a file is a symlink and return an error on Add * rename Watcher to FileWatcher and remove symlink deref * add fsnotify@v1.5.1 * fix go mod * do not reset timer on errors, rename OS specific files * rename New func * events trigger on write and rename * add missing test * fix flaking tests * fix flaky test * check reconcile when removed * delete invalid file * fix test to create files with different mod time. * back date file instead of sleeping * add watching file in agent command. * fix watcher call to use new API * add configuration and stop watcher when server stop * add certs as watched files * move FileWatcher to the agent start instead of the command code * stop watcher before replacing it * save watched files in agent * add add and remove interfaces to the file watcher * fix remove to not return an error * use `Add` and `Remove` to update certs files * fix tests * close events channel on the file watcher even when the context is done * extract `NotAutoReloadableRuntimeConfig` is a separate struct * fix linter errors * add Ca configs and outgoing verify to the not auto reloadable config * add some logs and fix to use background context * add tests to auto-config reload * remove stale test * add tests to changes to config files * add check to see if old cert files still trigger updates * rename `NotAutoReloadableRuntimeConfig` to `StaticRuntimeConfig` * fix to re add both key and cert file. Add test to cover this case. * review suggestion Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * add check to static runtime config changes * fix test * add changelog file * fix review comments * Apply suggestions from code review Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * update flag description Co-authored-by: FFMMM <FFMMM@users.noreply.github.com> * fix compilation error * add static runtime config support * fix test * fix review comments * fix log test * Update .changelog/12329.txt Co-authored-by: Dan Upton <daniel@floppy.co> * transfer tests to runtime_test.go * fix filewatcher Replace to not deadlock. * avoid having lingering locks Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * split ReloadConfig func * fix warning message Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * convert `FileWatcher` into an interface * fix compilation errors * fix tests * extract func for adding and removing files * add a coalesceTimer with a very small timer * extract coaelsce Timer and add a shim for testing * add tests to coalesceTimer fix to send remaining events * set `coalesceTimer` to 1 Second * support symlink, fix a nil deref. * fix compile error * fix compile error * refactor file watcher rate limiting to be a Watcher implementation * fix linter issue * fix runtime config * fix runtime test * fix flaky tests * fix compile error * Apply suggestions from code review Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> * fix agent New to return an error if File watcher New return an error * quit timer loop if ctx is canceled * Apply suggestions from code review Co-authored-by: Chris S. Kim <ckim@hashicorp.com> Co-authored-by: Ashwin Venkatesh <ashwin@hashicorp.com> Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> Co-authored-by: FFMMM <FFMMM@users.noreply.github.com> Co-authored-by: Daniel Upton <daniel@floppy.co> Co-authored-by: Chris S. Kim <ckim@hashicorp.com>
416 lines
11 KiB
Go
416 lines
11 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const defaultTimeout = 500 * time.Millisecond
|
|
|
|
func TestNewWatcher(t *testing.T) {
|
|
w, err := NewFileWatcher([]string{}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, w)
|
|
}
|
|
|
|
func TestWatcherRenameEvent(t *testing.T) {
|
|
|
|
fileTmp := createTempConfigFile(t, "temp_config3")
|
|
filepaths := []string{createTempConfigFile(t, "temp_config1"), createTempConfigFile(t, "temp_config2")}
|
|
wi, err := NewFileWatcher(filepaths, hclog.New(&hclog.LoggerOptions{}))
|
|
w := wi.(*fileWatcher)
|
|
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
require.NoError(t, err)
|
|
err = os.Rename(fileTmp, filepaths[0])
|
|
time.Sleep(w.reconcileTimeout + 50*time.Millisecond)
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(filepaths[0], w.eventsCh, defaultTimeout))
|
|
// make sure we consume all events
|
|
_ = assertEvent(filepaths[0], w.eventsCh, defaultTimeout)
|
|
}
|
|
|
|
func TestWatcherAddRemove(t *testing.T) {
|
|
var filepaths []string
|
|
wi, err := NewFileWatcher(filepaths, hclog.New(&hclog.LoggerOptions{}))
|
|
w := wi.(*fileWatcher)
|
|
require.NoError(t, err)
|
|
file1 := createTempConfigFile(t, "temp_config1")
|
|
err = w.Add(file1)
|
|
require.NoError(t, err)
|
|
file2 := createTempConfigFile(t, "temp_config2")
|
|
err = w.Add(file2)
|
|
require.NoError(t, err)
|
|
w.Remove(file2)
|
|
_, ok := w.configFiles[file1]
|
|
require.True(t, ok)
|
|
_, ok = w.configFiles[file2]
|
|
require.False(t, ok)
|
|
|
|
}
|
|
|
|
func TestWatcherReplace(t *testing.T) {
|
|
var filepaths []string
|
|
wi, err := NewFileWatcher(filepaths, hclog.New(&hclog.LoggerOptions{}))
|
|
w := wi.(*fileWatcher)
|
|
require.NoError(t, err)
|
|
file1 := createTempConfigFile(t, "temp_config1")
|
|
err = w.Add(file1)
|
|
require.NoError(t, err)
|
|
file2 := createTempConfigFile(t, "temp_config2")
|
|
err = w.Replace(file1, file2)
|
|
require.NoError(t, err)
|
|
_, ok := w.configFiles[file1]
|
|
require.False(t, ok)
|
|
_, ok = w.configFiles[file2]
|
|
require.True(t, ok)
|
|
}
|
|
|
|
func TestWatcherAddWhileRunning(t *testing.T) {
|
|
var filepaths []string
|
|
wi, err := NewFileWatcher(filepaths, hclog.New(&hclog.LoggerOptions{}))
|
|
w := wi.(*fileWatcher)
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
file1 := createTempConfigFile(t, "temp_config1")
|
|
err = w.Add(file1)
|
|
require.NoError(t, err)
|
|
file2 := createTempConfigFile(t, "temp_config2")
|
|
err = w.Add(file2)
|
|
require.NoError(t, err)
|
|
w.Remove(file2)
|
|
require.Len(t, w.configFiles, 1)
|
|
_, ok := w.configFiles[file1]
|
|
require.True(t, ok)
|
|
_, ok = w.configFiles[file2]
|
|
require.False(t, ok)
|
|
}
|
|
|
|
func TestWatcherRemoveNotFound(t *testing.T) {
|
|
var filepaths []string
|
|
w, err := NewFileWatcher(filepaths, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
file := createTempConfigFile(t, "temp_config2")
|
|
w.Remove(file)
|
|
}
|
|
|
|
func TestWatcherAddNotExist(t *testing.T) {
|
|
|
|
file := testutil.TempFile(t, "temp_config")
|
|
filename := file.Name() + randomStr(16)
|
|
w, err := NewFileWatcher([]string{filename}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.Error(t, err, "no such file or directory")
|
|
require.Nil(t, w)
|
|
}
|
|
|
|
func TestEventWatcherWrite(t *testing.T) {
|
|
|
|
file := testutil.TempFile(t, "temp_config")
|
|
_, err := file.WriteString("test config")
|
|
require.NoError(t, err)
|
|
err = file.Sync()
|
|
require.NoError(t, err)
|
|
w, err := NewFileWatcher([]string{file.Name()}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
_, err = file.WriteString("test config 2")
|
|
require.NoError(t, err)
|
|
err = file.Sync()
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(file.Name(), w.EventsCh(), defaultTimeout))
|
|
}
|
|
|
|
func TestEventWatcherRead(t *testing.T) {
|
|
|
|
filepath := createTempConfigFile(t, "temp_config1")
|
|
w, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
_, err = os.ReadFile(filepath)
|
|
require.NoError(t, err)
|
|
require.Error(t, assertEvent(filepath, w.EventsCh(), defaultTimeout), "timedout waiting for event")
|
|
}
|
|
|
|
func TestEventWatcherChmod(t *testing.T) {
|
|
file := testutil.TempFile(t, "temp_config")
|
|
defer func() {
|
|
err := file.Close()
|
|
require.NoError(t, err)
|
|
}()
|
|
_, err := file.WriteString("test config")
|
|
require.NoError(t, err)
|
|
err = file.Sync()
|
|
require.NoError(t, err)
|
|
|
|
w, err := NewFileWatcher([]string{file.Name()}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
err = file.Chmod(0777)
|
|
require.NoError(t, err)
|
|
require.Error(t, assertEvent(file.Name(), w.EventsCh(), defaultTimeout), "timedout waiting for event")
|
|
}
|
|
|
|
func TestEventWatcherRemoveCreate(t *testing.T) {
|
|
|
|
filepath := createTempConfigFile(t, "temp_config1")
|
|
w, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
require.NoError(t, err)
|
|
err = os.Remove(filepath)
|
|
require.NoError(t, err)
|
|
recreated, err := os.Create(filepath)
|
|
require.NoError(t, err)
|
|
_, err = recreated.WriteString("config 2")
|
|
require.NoError(t, err)
|
|
err = recreated.Sync()
|
|
require.NoError(t, err)
|
|
// this an event coming from the reconcile loop
|
|
require.NoError(t, assertEvent(filepath, w.EventsCh(), defaultTimeout))
|
|
}
|
|
|
|
func TestEventWatcherMove(t *testing.T) {
|
|
|
|
filepath := createTempConfigFile(t, "temp_config1")
|
|
|
|
w, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
filepath2 := createTempConfigFile(t, "temp_config2")
|
|
err = os.Rename(filepath2, filepath)
|
|
time.Sleep(timeoutDuration + 50*time.Millisecond)
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(filepath, w.EventsCh(), defaultTimeout))
|
|
}
|
|
}
|
|
|
|
func TestEventReconcileMove(t *testing.T) {
|
|
filepath := createTempConfigFile(t, "temp_config1")
|
|
filepath2 := createTempConfigFile(t, "temp_config2")
|
|
err := os.Chtimes(filepath, time.Now(), time.Now().Add(-1*time.Second))
|
|
require.NoError(t, err)
|
|
wi, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
w := wi.(*fileWatcher)
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
// remove the file from the internal watcher to only trigger the reconcile
|
|
err = w.watcher.Remove(filepath)
|
|
require.NoError(t, err)
|
|
|
|
err = os.Rename(filepath2, filepath)
|
|
time.Sleep(timeoutDuration + 50*time.Millisecond)
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(filepath, w.EventsCh(), 2000*time.Millisecond))
|
|
}
|
|
|
|
func TestEventWatcherDirCreateRemove(t *testing.T) {
|
|
filepath := testutil.TempDir(t, "temp_config1")
|
|
w, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
for i := 0; i < 1; i++ {
|
|
name := filepath + "/" + randomStr(20)
|
|
file, err := os.Create(name)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(filepath, w.EventsCh(), defaultTimeout))
|
|
|
|
err = os.Remove(name)
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(filepath, w.EventsCh(), defaultTimeout))
|
|
}
|
|
}
|
|
|
|
func TestEventWatcherDirMove(t *testing.T) {
|
|
filepath := testutil.TempDir(t, "temp_config1")
|
|
|
|
name := filepath + "/" + randomStr(20)
|
|
file, err := os.Create(name)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
w, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
filepathTmp := createTempConfigFile(t, "temp_config2")
|
|
err = os.Rename(filepathTmp, name)
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(filepath, w.EventsCh(), defaultTimeout))
|
|
}
|
|
}
|
|
|
|
func TestEventWatcherDirMoveTrim(t *testing.T) {
|
|
filepath := testutil.TempDir(t, "temp_config1")
|
|
|
|
name := filepath + "/" + randomStr(20)
|
|
file, err := os.Create(name)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
w, err := NewFileWatcher([]string{filepath + "/"}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
filepathTmp := createTempConfigFile(t, "temp_config2")
|
|
err = os.Rename(filepathTmp, name)
|
|
require.NoError(t, err)
|
|
require.NoError(t, assertEvent(filepath, w.EventsCh(), defaultTimeout))
|
|
}
|
|
}
|
|
|
|
// Consul do not support configuration in sub-directories
|
|
func TestEventWatcherSubDirMove(t *testing.T) {
|
|
filepath := testutil.TempDir(t, "temp_config1")
|
|
err := os.Mkdir(filepath+"/temp", 0777)
|
|
require.NoError(t, err)
|
|
name := filepath + "/temp/" + randomStr(20)
|
|
file, err := os.Create(name)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
w, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
defer func() {
|
|
_ = w.Stop()
|
|
}()
|
|
|
|
for i := 0; i < 2; i++ {
|
|
filepathTmp := createTempConfigFile(t, "temp_config2")
|
|
err = os.Rename(filepathTmp, name)
|
|
require.NoError(t, err)
|
|
require.Error(t, assertEvent(filepath, w.EventsCh(), defaultTimeout), "timedout waiting for event")
|
|
}
|
|
}
|
|
|
|
func TestEventWatcherDirRead(t *testing.T) {
|
|
filepath := testutil.TempDir(t, "temp_config1")
|
|
|
|
name := filepath + "/" + randomStr(20)
|
|
file, err := os.Create(name)
|
|
require.NoError(t, err)
|
|
err = file.Close()
|
|
require.NoError(t, err)
|
|
w, err := NewFileWatcher([]string{filepath}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
w.Start(context.Background())
|
|
t.Cleanup(func() {
|
|
_ = w.Stop()
|
|
})
|
|
|
|
_, err = os.ReadFile(name)
|
|
require.NoError(t, err)
|
|
require.Error(t, assertEvent(filepath, w.EventsCh(), defaultTimeout), "timedout waiting for event")
|
|
}
|
|
|
|
func TestEventWatcherMoveSoftLink(t *testing.T) {
|
|
|
|
filepath := createTempConfigFile(t, "temp_config1")
|
|
tempDir := testutil.TempDir(t, "temp_dir")
|
|
name := tempDir + "/" + randomStr(20)
|
|
err := os.Symlink(filepath, name)
|
|
require.NoError(t, err)
|
|
|
|
w, err := NewFileWatcher([]string{name}, hclog.New(&hclog.LoggerOptions{}))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, w)
|
|
|
|
}
|
|
|
|
func assertEvent(name string, watcherCh chan *FileWatcherEvent, timeout time.Duration) error {
|
|
select {
|
|
case ev := <-watcherCh:
|
|
if ev.Filenames[0] != name && !strings.Contains(ev.Filenames[0], name) {
|
|
return fmt.Errorf("filename do not match %s %s", ev.Filenames[0], name)
|
|
}
|
|
return nil
|
|
case <-time.After(timeout):
|
|
return fmt.Errorf("timedout waiting for event")
|
|
}
|
|
}
|
|
|
|
func createTempConfigFile(t *testing.T, filename string) string {
|
|
file := testutil.TempFile(t, filename)
|
|
|
|
_, err1 := file.WriteString("test config")
|
|
err2 := file.Close()
|
|
|
|
require.NoError(t, err1)
|
|
require.NoError(t, err2)
|
|
|
|
return file.Name()
|
|
}
|
|
|
|
func randomStr(length int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyz" +
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
var seededRand *rand.Rand = rand.New(
|
|
rand.NewSource(time.Now().UnixNano()))
|
|
b := make([]byte, length)
|
|
for i := range b {
|
|
b[i] = charset[seededRand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|