consul/agent/config/file_watcher_test.go
hashicorp-copywrite[bot] 5fb9df1640
[COMPLIANCE] License changes (#18443)
* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 09:12:13 -04:00

419 lines
11 KiB
Go

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