// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 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) }