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>
300 lines
8 KiB
Go
300 lines
8 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/hashicorp/go-hclog"
|
|
)
|
|
|
|
const timeoutDuration = 200 * time.Millisecond
|
|
|
|
type Watcher interface {
|
|
Start(ctx context.Context)
|
|
Stop() error
|
|
Add(filename string) error
|
|
Remove(filename string)
|
|
Replace(oldFile, newFile string) error
|
|
EventsCh() chan *FileWatcherEvent
|
|
}
|
|
|
|
type fileWatcher struct {
|
|
watcher *fsnotify.Watcher
|
|
configFiles map[string]*watchedFile
|
|
configFilesLock sync.RWMutex
|
|
logger hclog.Logger
|
|
reconcileTimeout time.Duration
|
|
cancel context.CancelFunc
|
|
done chan interface{}
|
|
stopOnce sync.Once
|
|
|
|
//eventsCh Channel where an event will be emitted when a file change is detected
|
|
// a call to Start is needed before any event is emitted
|
|
// after a Call to Stop succeed, the channel will be closed
|
|
eventsCh chan *FileWatcherEvent
|
|
}
|
|
|
|
type watchedFile struct {
|
|
modTime time.Time
|
|
}
|
|
|
|
type FileWatcherEvent struct {
|
|
Filenames []string
|
|
}
|
|
|
|
//NewFileWatcher create a file watcher that will watch all the files/folders from configFiles
|
|
// if success a fileWatcher will be returned and a nil error
|
|
// otherwise an error and a nil fileWatcher are returned
|
|
func NewFileWatcher(configFiles []string, logger hclog.Logger) (Watcher, error) {
|
|
ws, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
w := &fileWatcher{
|
|
watcher: ws,
|
|
logger: logger.Named("file-watcher"),
|
|
configFiles: make(map[string]*watchedFile),
|
|
eventsCh: make(chan *FileWatcherEvent),
|
|
reconcileTimeout: timeoutDuration,
|
|
done: make(chan interface{}),
|
|
}
|
|
for _, f := range configFiles {
|
|
err = w.Add(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error adding file %q: %w", f, err)
|
|
}
|
|
}
|
|
|
|
return w, nil
|
|
}
|
|
|
|
// Start start a file watcher, with a copy of the passed context.
|
|
// calling Start multiple times is a noop
|
|
func (w *fileWatcher) Start(ctx context.Context) {
|
|
if w.cancel == nil {
|
|
cancelCtx, cancel := context.WithCancel(ctx)
|
|
w.cancel = cancel
|
|
go w.watch(cancelCtx)
|
|
}
|
|
}
|
|
|
|
// Stop the file watcher
|
|
// calling Stop multiple times is a noop, Stop must be called after a Start
|
|
func (w *fileWatcher) Stop() error {
|
|
var err error
|
|
w.stopOnce.Do(func() {
|
|
w.cancel()
|
|
<-w.done
|
|
err = w.watcher.Close()
|
|
})
|
|
return err
|
|
}
|
|
|
|
// Add a file to the file watcher
|
|
// Add will lock the file watcher during the add
|
|
func (w *fileWatcher) Add(filename string) error {
|
|
filename = filepath.Clean(filename)
|
|
w.logger.Trace("adding file", "file", filename)
|
|
if err := w.watcher.Add(filename); err != nil {
|
|
return err
|
|
}
|
|
modTime, err := w.getFileModifiedTime(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.addFile(filename, modTime)
|
|
return nil
|
|
}
|
|
|
|
// Remove a file from the file watcher
|
|
// Remove will lock the file watcher during the remove
|
|
func (w *fileWatcher) Remove(filename string) {
|
|
w.removeFile(filename)
|
|
}
|
|
|
|
// Replace a file in the file watcher
|
|
// Replace will lock the file watcher during the replace
|
|
func (w *fileWatcher) Replace(oldFile, newFile string) error {
|
|
if oldFile == newFile {
|
|
return nil
|
|
}
|
|
newFile = filepath.Clean(newFile)
|
|
w.logger.Trace("adding file", "file", newFile)
|
|
if err := w.watcher.Add(newFile); err != nil {
|
|
return err
|
|
}
|
|
modTime, err := w.getFileModifiedTime(newFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.replaceFile(oldFile, newFile, modTime)
|
|
return nil
|
|
}
|
|
|
|
func (w *fileWatcher) replaceFile(oldFile, newFile string, modTime time.Time) {
|
|
w.configFilesLock.Lock()
|
|
defer w.configFilesLock.Unlock()
|
|
delete(w.configFiles, oldFile)
|
|
w.configFiles[newFile] = &watchedFile{modTime: modTime}
|
|
}
|
|
|
|
func (w *fileWatcher) addFile(filename string, modTime time.Time) {
|
|
w.configFilesLock.Lock()
|
|
defer w.configFilesLock.Unlock()
|
|
w.configFiles[filename] = &watchedFile{modTime: modTime}
|
|
}
|
|
|
|
func (w *fileWatcher) removeFile(filename string) {
|
|
w.configFilesLock.Lock()
|
|
defer w.configFilesLock.Unlock()
|
|
delete(w.configFiles, filename)
|
|
}
|
|
|
|
func (w *fileWatcher) EventsCh() chan *FileWatcherEvent {
|
|
return w.eventsCh
|
|
}
|
|
|
|
func (w *fileWatcher) watch(ctx context.Context) {
|
|
ticker := time.NewTicker(w.reconcileTimeout)
|
|
defer ticker.Stop()
|
|
defer close(w.done)
|
|
defer close(w.eventsCh)
|
|
|
|
for {
|
|
select {
|
|
case event, ok := <-w.watcher.Events:
|
|
if !ok {
|
|
w.logger.Error("watcher event channel is closed")
|
|
return
|
|
}
|
|
w.logger.Trace("received watcher event", "event", event)
|
|
if err := w.handleEvent(ctx, event); err != nil {
|
|
w.logger.Error("error handling watcher event", "error", err, "event", event)
|
|
}
|
|
case _, ok := <-w.watcher.Errors:
|
|
if !ok {
|
|
w.logger.Error("watcher error channel is closed")
|
|
return
|
|
}
|
|
case <-ticker.C:
|
|
w.reconcile(ctx)
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *fileWatcher) handleEvent(ctx context.Context, event fsnotify.Event) error {
|
|
w.logger.Trace("event received ", "filename", event.Name, "OP", event.Op)
|
|
// we only want Create and Remove events to avoid triggering a reload on file modification
|
|
if !isCreateEvent(event) && !isRemoveEvent(event) && !isWriteEvent(event) && !isRenameEvent(event) {
|
|
return nil
|
|
}
|
|
filename := filepath.Clean(event.Name)
|
|
configFile, basename, ok := w.isWatched(filename)
|
|
if !ok {
|
|
return fmt.Errorf("file %s is not watched", event.Name)
|
|
}
|
|
|
|
// we only want to update mod time and re-add if the event is on the watched file itself
|
|
if filename == basename {
|
|
if isRemoveEvent(event) {
|
|
// If the file was removed, try to reconcile and see if anything changed.
|
|
w.logger.Trace("attempt a reconcile ", "filename", event.Name, "OP", event.Op)
|
|
configFile.modTime = time.Time{}
|
|
w.reconcile(ctx)
|
|
}
|
|
}
|
|
if isCreateEvent(event) || isWriteEvent(event) || isRenameEvent(event) {
|
|
w.logger.Trace("call the handler", "filename", event.Name, "OP", event.Op)
|
|
select {
|
|
case w.eventsCh <- &FileWatcherEvent{Filenames: []string{filename}}:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *fileWatcher) isWatched(filename string) (*watchedFile, string, bool) {
|
|
path := filename
|
|
w.configFilesLock.RLock()
|
|
configFile, ok := w.configFiles[path]
|
|
w.configFilesLock.RUnlock()
|
|
if ok {
|
|
return configFile, path, true
|
|
}
|
|
|
|
stat, err := os.Lstat(filename)
|
|
|
|
// if the error is a not exist still try to find if the event for a configured file
|
|
if os.IsNotExist(err) || (!stat.IsDir() && stat.Mode()&os.ModeSymlink == 0) {
|
|
w.logger.Trace("not a dir and not a symlink to a dir")
|
|
// try to see if the watched path is the parent dir
|
|
newPath := filepath.Dir(path)
|
|
w.logger.Trace("get dir", "dir", newPath)
|
|
w.configFilesLock.RLock()
|
|
configFile, ok = w.configFiles[newPath]
|
|
w.configFilesLock.RUnlock()
|
|
}
|
|
return configFile, path, ok
|
|
}
|
|
|
|
func (w *fileWatcher) reconcile(ctx context.Context) {
|
|
w.configFilesLock.Lock()
|
|
defer w.configFilesLock.Unlock()
|
|
for filename, configFile := range w.configFiles {
|
|
newModTime, err := w.getFileModifiedTime(filename)
|
|
if err != nil {
|
|
w.logger.Error("failed to get file modTime", "file", filename, "err", err)
|
|
continue
|
|
}
|
|
|
|
err = w.watcher.Add(filename)
|
|
if err != nil {
|
|
w.logger.Error("failed to add file to watcher", "file", filename, "err", err)
|
|
continue
|
|
}
|
|
if !configFile.modTime.Equal(newModTime) {
|
|
w.logger.Trace("call the handler", "filename", filename, "old modTime", configFile.modTime, "new modTime", newModTime)
|
|
configFile.modTime = newModTime
|
|
select {
|
|
case w.eventsCh <- &FileWatcherEvent{Filenames: []string{filename}}:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func isCreateEvent(event fsnotify.Event) bool {
|
|
return event.Op&fsnotify.Create == fsnotify.Create
|
|
}
|
|
|
|
func isRemoveEvent(event fsnotify.Event) bool {
|
|
return event.Op&fsnotify.Remove == fsnotify.Remove
|
|
}
|
|
|
|
func isWriteEvent(event fsnotify.Event) bool {
|
|
return event.Op&fsnotify.Write == fsnotify.Write
|
|
}
|
|
|
|
func isRenameEvent(event fsnotify.Event) bool {
|
|
return event.Op&fsnotify.Rename == fsnotify.Rename
|
|
}
|
|
|
|
func (w *fileWatcher) getFileModifiedTime(filename string) (time.Time, error) {
|
|
fileInfo, err := os.Stat(filename)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
|
|
return fileInfo.ModTime(), err
|
|
}
|