-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add filewatch package to template-validator
This package was copied directly from vm-console-proxy: https://github.com/kubevirt/vm-console-proxy/tree/main/pkg/filewatch In a future commit, it will replace file watch logic in internal/template-validator/tlsinfo. We do this to simplify watching multiple directories with TRS certificate and TLS configuration. Signed-off-by: Andrej Krejcir <akrejcir@redhat.com>
- Loading branch information
Showing
2 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package filewatch | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"sync" | ||
"sync/atomic" | ||
|
||
"github.com/fsnotify/fsnotify" | ||
) | ||
|
||
type Watch interface { | ||
Add(path string, callback func()) error | ||
Run(done <-chan struct{}) error | ||
IsRunning() bool | ||
} | ||
|
||
func New() Watch { | ||
return &watch{ | ||
callbacks: make(map[string]func()), | ||
} | ||
} | ||
|
||
type watch struct { | ||
lock sync.Mutex | ||
callbacks map[string]func() | ||
running atomic.Bool | ||
} | ||
|
||
var _ Watch = &watch{} | ||
|
||
func (w *watch) Add(path string, callback func()) error { | ||
w.lock.Lock() | ||
defer w.lock.Unlock() | ||
|
||
if w.running.Load() { | ||
return fmt.Errorf("cannot add to a running watch") | ||
} | ||
|
||
w.callbacks[path] = callback | ||
return nil | ||
} | ||
|
||
func (w *watch) Run(done <-chan struct{}) error { | ||
watcher, err := fsnotify.NewWatcher() | ||
if err != nil { | ||
return fmt.Errorf("could not create fsnotify.Watcher: %w", err) | ||
} | ||
// watcher.Close() never returns an error | ||
defer func() { _ = watcher.Close() }() | ||
|
||
func() { | ||
// Before setting running to true, we need to acquire the lock, | ||
// because Add() method may be running concurrently. | ||
w.lock.Lock() | ||
defer w.lock.Unlock() | ||
w.running.Store(true) | ||
}() | ||
// Setting running to false is ok without a lock. | ||
defer w.running.Store(false) | ||
|
||
err = w.addCallbacks(watcher) | ||
if err != nil { | ||
return fmt.Errorf("could not add callbacks: %w", err) | ||
} | ||
|
||
return w.processEvents(watcher, done) | ||
} | ||
|
||
func (w *watch) IsRunning() bool { | ||
return w.running.Load() | ||
} | ||
|
||
func (w *watch) addCallbacks(watcher *fsnotify.Watcher) error { | ||
for path := range w.callbacks { | ||
err := watcher.Add(path) | ||
if err != nil { | ||
return fmt.Errorf("failed watch %s: %w", path, err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (w *watch) processEvents(watcher *fsnotify.Watcher, done <-chan struct{}) error { | ||
for { | ||
select { | ||
case <-done: | ||
return nil | ||
|
||
case event, ok := <-watcher.Events: | ||
if !ok { | ||
return nil | ||
} | ||
w.handleEvent(event) | ||
|
||
case err, ok := <-watcher.Errors: | ||
if !ok { | ||
return nil | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (w *watch) handleEvent(event fsnotify.Event) { | ||
const modificationEvents = fsnotify.Create | fsnotify.Write | fsnotify.Remove | ||
if event.Op&modificationEvents == 0 { | ||
return | ||
} | ||
|
||
for path, callback := range w.callbacks { | ||
if strings.HasPrefix(event.Name, path) { | ||
callback() | ||
} | ||
} | ||
} |
137 changes: 137 additions & 0 deletions
137
internal/template-validator/filewatch/filewatch_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package filewatch | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"runtime" | ||
"sync/atomic" | ||
"testing" | ||
"time" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("Filewatch", func() { | ||
Context("watching file", func() { | ||
var ( | ||
callback func() | ||
tempFileName string | ||
) | ||
|
||
BeforeEach(func() { | ||
tmpDir := GinkgoT().TempDir() | ||
tempFileName = filepath.Join(tmpDir, "test-file") | ||
Expect(os.WriteFile(tempFileName, []byte("test content"), 0777)).To(Succeed()) | ||
|
||
callback = func() { | ||
defer GinkgoRecover() | ||
panic("callback was not set") | ||
} | ||
|
||
startWatch(tempFileName, func() { callback() }) | ||
}) | ||
|
||
It("should call callback on file change", func() { | ||
called := atomic.Bool{} | ||
callback = func() { called.Store(true) } | ||
|
||
Expect(os.WriteFile(tempFileName, []byte("different content"), 0777)).To(Succeed()) | ||
|
||
Eventually(called.Load, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
}) | ||
|
||
It("should call callback on file deletion", func() { | ||
called := atomic.Bool{} | ||
callback = func() { called.Store(true) } | ||
|
||
Expect(os.Remove(tempFileName)).ToNot(HaveOccurred()) | ||
|
||
Eventually(called.Load, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
}) | ||
}) | ||
|
||
Context("watching directory", func() { | ||
var ( | ||
callback func() | ||
tempDirName string | ||
) | ||
|
||
BeforeEach(func() { | ||
tempDirName = GinkgoT().TempDir() | ||
|
||
callback = func() { | ||
defer GinkgoRecover() | ||
panic("callback was not set") | ||
} | ||
|
||
startWatch(tempDirName, func() { callback() }) | ||
}) | ||
|
||
It("should call callback on file creation", func() { | ||
called := atomic.Bool{} | ||
callback = func() { called.Store(true) } | ||
|
||
Expect(os.WriteFile(filepath.Join(tempDirName, "created-file"), []byte("content"), 0777)).To(Succeed()) | ||
|
||
Eventually(called.Load, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
}) | ||
|
||
It("should call callback on file change", func() { | ||
called := atomic.Bool{} | ||
callback = func() { called.Store(true) } | ||
|
||
const filename = "test-file" | ||
Expect(os.WriteFile(filepath.Join(tempDirName, filename), []byte("content"), 0777)).To(Succeed()) | ||
|
||
Eventually(called.Load, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
|
||
called.Store(false) | ||
|
||
Expect(os.WriteFile(filepath.Join(tempDirName, filename), []byte("updated content"), 0777)).To(Succeed()) | ||
|
||
Eventually(called.Load, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
}) | ||
|
||
It("should call callback on file deletion", func() { | ||
called := atomic.Bool{} | ||
callback = func() { called.Store(true) } | ||
|
||
const filename = "test-file" | ||
Expect(os.WriteFile(filepath.Join(tempDirName, filename), []byte("content"), 0777)).To(Succeed()) | ||
|
||
Eventually(called.Load, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
|
||
called.Store(false) | ||
|
||
Expect(os.Remove(filepath.Join(tempDirName, filename))).ToNot(HaveOccurred()) | ||
|
||
Eventually(called.Load, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
}) | ||
}) | ||
}) | ||
|
||
func startWatch(path string, callback func()) { | ||
testWatch := New() | ||
err := testWatch.Add(path, callback) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
done := make(chan struct{}) | ||
DeferCleanup(func() { | ||
close(done) | ||
}) | ||
|
||
go func() { | ||
defer GinkgoRecover() | ||
Expect(testWatch.Run(done)).To(Succeed()) | ||
}() | ||
|
||
// Wait for a short time to let the watch goroutine run | ||
runtime.Gosched() | ||
Eventually(testWatch.IsRunning, time.Second, 50*time.Millisecond).Should(BeTrue()) | ||
} | ||
|
||
func TestFilewatch(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "Filewatch Suite") | ||
} |