Skip to content

feat(certs): add cert watcher with fsnotify and SHA-256 polling#2561

Draft
fseldow wants to merge 1 commit into
notaryproject:mainfrom
fseldow:xinhl/alpha2-cert-watcher
Draft

feat(certs): add cert watcher with fsnotify and SHA-256 polling#2561
fseldow wants to merge 1 commit into
notaryproject:mainfrom
fseldow:xinhl/alpha2-cert-watcher

Conversation

@fseldow

@fseldow fseldow commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a trust store watcher that combines fsnotify events with SHA-256 polling
  • debounce certificate reloads by 2 seconds and support watching additional paths
  • reload filesystem provider certificates when trust store files change, with focused tests

Testing

  • go test ./internal/verifier/truststore ./internal/verifier/keyprovider/filesystemprovider

Copilot AI review requested due to automatic review settings June 1, 2026 11:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a filesystem-backed trust store watcher so certificate providers can automatically reload cached certificates when files change.

Changes:

  • Introduces truststore.Watcher based on fsnotify with a polling fallback and debouncing.
  • Updates the filesystem keyprovider to keep an in-memory certificate cache that reloads on watch events.
  • Adds/updates tests to cover watcher behaviors and certificate reload after file changes/removal.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
internal/verifier/truststore/watcher.go New watcher implementation for detecting trust store changes (fsnotify + polling).
internal/verifier/truststore/watcher_test.go New tests covering watcher construction, callback triggering, polling detection, and stop behavior.
internal/verifier/keyprovider/filesystemprovider/register.go Adds watcher-backed certificate reloading with concurrency protection.
internal/verifier/keyprovider/filesystemprovider/register_test.go Adjusts certificate tests and adds coverage for reload after file removal.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +136 to +154
case event, ok := <-w.watcher.Events:
if !ok {
return
}
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 {
continue
}
logrus.Debugf("trust store watcher event: %s %s", event.Op, event.Name)
if event.Op&(fsnotify.Remove|fsnotify.Rename) != 0 {
_ = w.watcher.Add(event.Name)
}
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceTimer = time.AfterFunc(debounceInterval, func() {
logrus.Infof("trust store cert change detected: %s", event.Name)
w.callback()
w.snapshotHashes()
})
Comment thread internal/verifier/truststore/watcher.go Outdated
Comment on lines +198 to +217
func (w *Watcher) checkPathChanged(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
if info.IsDir() {
changed := false
_ = filepath.WalkDir(path, func(filePath string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
if w.fileHashChanged(filePath) {
changed = true
}
return nil
})
return changed
}
return w.fileHashChanged(path)
}
Comment thread internal/verifier/truststore/watcher.go Outdated
Comment on lines +231 to +260
func (w *Watcher) snapshotHashes() {
w.mu.Lock()
defer w.mu.Unlock()

for _, path := range w.paths {
info, err := os.Stat(path)
if err != nil {
continue
}
if info.IsDir() {
_ = filepath.WalkDir(path, func(filePath string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
data, err := os.ReadFile(filePath)
if err != nil {
return nil
}
w.hashes[filePath] = sha256.Sum256(data)
return nil
})
continue
}
data, err := os.ReadFile(path)
if err != nil {
continue
}
w.hashes[path] = sha256.Sum256(data)
}
}
Comment on lines +94 to +113
func (w *Watcher) AddPath(path string) error {
w.mu.Lock()
for _, existing := range w.paths {
if existing == path {
w.mu.Unlock()
return nil
}
}
w.mu.Unlock()

if err := w.addPath(path); err != nil {
return err
}

w.mu.Lock()
w.paths = append(w.paths, path)
w.mu.Unlock()
w.snapshotHashes()
return nil
}
Comment on lines +65 to +78
watcher, err := verifiertruststore.NewWatcher(paths, func() {
if err := provider.reloadCertificates(); err != nil {
logrus.WithError(err).Error("failed to reload trust store certificates")
}
})
if err != nil {
return nil, fmt.Errorf("failed to create trust store watcher: %w", err)
}
if err := watcher.Start(); err != nil {
watcher.Stop()
return nil, fmt.Errorf("failed to start trust store watcher: %w", err)
}
provider.watcher = watcher
return provider, nil
Comment on lines +58 to +66
time.Sleep(100 * time.Millisecond)
if err := os.WriteFile(certFile, []byte("rotated-cert"), 0o644); err != nil {
t.Fatal(err)
}

time.Sleep(3 * time.Second)
if calls.Load() == 0 {
t.Fatal("expected callback after cert write")
}
Comment on lines +92 to 94
if len(certs) == 0 {
t.Fatalf("expected at least one certificate, got %d", len(certs))
}
Comment on lines +105 to 107
if len(certs) == 0 {
t.Fatalf("expected at least one certificate, got %d", len(certs))
}
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.56425% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.77%. Comparing base (c6a515a) to head (107987d).

Files with missing lines Patch % Lines
internal/verifier/truststore/watcher.go 82.55% 13 Missing and 13 partials ⚠️
...erifier/keyprovider/filesystemprovider/register.go 76.66% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2561      +/-   ##
==========================================
+ Coverage   77.62%   77.77%   +0.15%     
==========================================
  Files         105      106       +1     
  Lines        4657     4824     +167     
==========================================
+ Hits         3615     3752     +137     
- Misses        893      908      +15     
- Partials      149      164      +15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: xinhl <xinhl@microsoft.com>
@fseldow fseldow force-pushed the xinhl/alpha2-cert-watcher branch from 26049cf to 107987d Compare June 1, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants