You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/agent/config/file_watcher_test.go

419 lines
11 KiB

// 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)
}