Skip to content

Instantly share code, notes, and snippets.

@henvic
Last active April 5, 2019 16:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save henvic/4da7dc2fac692388d0d5c0747aa54e7d to your computer and use it in GitHub Desktop.
Save henvic/4da7dc2fac692388d0d5c0747aa54e7d to your computer and use it in GitHub Desktop.
certlistener (old)
mocks/certs/example.org

See fsnotify/fsnotify#9

Create empty files

  • mocks/certs/example.com/keyfullchain.pem
  • mocks/certs/example.net/keyfullchain.pem
package certlistener
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"github.com/rs/zerolog"
)
const (
// Added event
Added Event = "added"
// Modified event
Modified Event = "modified"
// Deleted event
Deleted Event = "deleted"
)
var logger = zerolog.New(os.Stderr).With().
Str("from", "certslistener").
Logger()
// Watcher to listen for certificate file changes
// with a non-recursively structure like: /dir/<directory>/<name>
// i.e., /certificates/example.net/keyfullchain.pem
type Watcher struct {
ctx context.Context
dir string
name string // usually keyfullchain.pem
compare string
interval time.Duration
cache map[string]cert
mutex sync.RWMutex
changes chan Change
err chan error
}
type cert struct {
path string
fileInfo os.FileInfo
}
// Change for the given file
type Change struct {
Filename string
Event Event
}
// Event that happened (added, modified, deleted)
type Event string
// Listen for changes
func (w *Watcher) Listen(ctx context.Context,
dir string,
name string,
interval time.Duration) (chan Change, chan error) {
logger.Debug().
Str("listen-directory", dir).
Str("listen-certificate-filename", name).
Msg("starting to listen")
w.ctx = ctx
dir, err := filepath.Abs(dir)
if err != nil {
logger.Error().Msg("error getting path for certificates directory")
w.err <- err
return nil, w.err
}
w.dir = dir
w.name = name
w.cache = map[string]cert{}
w.interval = interval
w.changes = make(chan Change)
w.err = make(chan error)
go func() {
w.listen()
}()
logger.Debug().Msg("started to listen to certificates changes on the background")
return w.changes, w.err
}
func (w *Watcher) listen() {
ticker := time.NewTicker(w.interval)
defer ticker.Stop()
logger.Info().Str("trigger", "initial").Msg("checking certificates")
w.checkChanges()
for {
select {
case <-ticker.C:
logger.Debug().Str("trigger", "ticker").Msg("checking certificates")
w.checkChanges()
case <-w.ctx.Done():
logger.Debug().Msg("stopping checking certificates after context cancel signal")
w.err <- w.ctx.Err()
ticker.Stop()
return
}
}
}
func (w *Watcher) checkChanges() {
w.mutex.RLock()
var original = w.cache
w.mutex.RUnlock()
var exists = map[string]bool{}
var ls, err = ioutil.ReadDir(w.dir)
if err != nil {
logger.Error().Str("listen-directory", w.dir).Msg("can't read certificates directory")
w.err <- err
return
}
logger.Debug().Msg("verifying added and modified certs")
for _, fi := range ls {
if !fi.IsDir() {
continue
}
certPath := filepath.Join(w.dir, fi.Name(), w.name)
switch err := w.checkFileChange(certPath, original); {
case os.IsNotExist(err):
exists[certPath] = false
case err != nil:
w.err <- err
return
default:
exists[certPath] = true
}
}
logger.Debug().Msg("verifying removed certs")
for path := range original {
if !exists[path] {
w.deleteChange(path)
}
}
return
}
func (w *Watcher) checkFileChange(path string, original map[string]cert) error {
logger.Debug().Str("certificate-path", path).Msg("checking certificate")
var fi, err = os.Stat(path)
if err != nil {
return err
}
var current = cert{
path: path,
fileInfo: fi,
}
switch prev, existed := original[path]; {
case !existed:
w.addChange(path, current)
case sameFile(prev, current):
default:
w.modifyChange(path, current)
}
return nil
}
func sameFile(d1, d2 cert) bool {
fi1 := d1.fileInfo
fi2 := d2.fileInfo
if fi1.ModTime() == fi2.ModTime() &&
fi1.Size() == fi2.Size() &&
os.SameFile(fi1, fi2) {
logger.Debug().Str("certificate-path", d1.path).Msg("certificate has not changed")
return true
}
logger.Debug().Str("certificate-path", d1.path).Msg("certificate has changed")
return false
}
func (w *Watcher) addChange(path string, d cert) {
logger.Debug().Str("certificate-path", path).Msg("adding certificate")
w.mutex.Lock()
w.cache[path] = d
w.mutex.Unlock()
w.changes <- Change{
Filename: path,
Event: Added,
}
}
func (w *Watcher) modifyChange(path string, d cert) {
logger.Debug().Str("certificate-path", path).Msg("modifying certificate")
w.mutex.Lock()
w.cache[path] = d
w.mutex.Unlock()
w.changes <- Change{
Filename: path,
Event: Modified,
}
}
func (w *Watcher) deleteChange(path string) {
logger.Debug().Str("certificate-path", path).Msg("deleting certificate")
w.mutex.Lock()
delete(w.cache, path)
w.mutex.Unlock()
w.changes <- Change{
Filename: path,
Event: Deleted,
}
}
package certlistener
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"time"
)
func TestWatcher(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
ctxWatchChanges, cancelWatchChanges := context.WithTimeout(context.Background(), 6*time.Second)
defer cancel()
defer cancelWatchChanges()
cleanupExampleOrg()
defer cleanupExampleOrg()
w := Watcher{}
cc, ec := w.Listen(ctx, "mocks/certs", "keyfullchain.pem", 80*time.Millisecond)
go createAndModifyAndDeleteExampleOrgCerts()
gotChanges, gotErrors := watchChanges(ctxWatchChanges, cc, ec)
mocksPath, _ := filepath.Abs("./mocks/certs")
// the certificates for example.com and example.net should exists on initialization time
// and the certificate for example.org is created during watch time
var wantChanges = []Change{
Change{
Filename: filepath.Join(mocksPath, "example.com/keyfullchain.pem"),
Event: Added,
},
Change{
Filename: filepath.Join(mocksPath, "example.net/keyfullchain.pem"),
Event: Added,
},
Change{
Filename: filepath.Join(mocksPath, "example.org/keyfullchain.pem"),
Event: Added,
},
Change{
Filename: filepath.Join(mocksPath, "example.org/keyfullchain.pem"),
Event: Modified,
},
Change{
Filename: filepath.Join(mocksPath, "example.org/keyfullchain.pem"),
Event: Deleted,
},
}
var wantErrors = []error{
ctx.Err(), // we are exiting the listener using context timeout
}
if !reflect.DeepEqual(gotChanges, wantChanges) {
t.Errorf("Expected changes to be %v, got %v instead", wantChanges, gotChanges)
}
if !reflect.DeepEqual(gotErrors, wantErrors) {
t.Errorf("Expected watch errors to be %v, got %v instead", wantErrors, gotErrors)
}
}
func createAndModifyAndDeleteExampleOrgCerts() {
if err := os.Mkdir("mocks/certs/example.org", 0777); err != nil {
panic(err)
}
if err := ioutil.WriteFile(filepath.Join("mocks/certs/example.org/keyfullchain.pem"), []byte("abc"), 0644); err != nil {
panic(err)
}
time.Sleep(200 * time.Millisecond)
if err := ioutil.WriteFile(filepath.Join("mocks/certs/example.org/keyfullchain.pem"), []byte("xyz..."), 0644); err != nil {
panic(err)
}
time.Sleep(200 * time.Millisecond)
if err := os.Remove(filepath.Join("mocks/certs/example.org/keyfullchain.pem")); err != nil {
panic(err)
}
}
func cleanupExampleOrg() {
if err := os.RemoveAll("mocks/certs/example.org"); err != nil {
panic(err)
}
}
func TestWatcherOnMissingRootDirectory(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
ctxWatchChanges, cancelWatchChanges := context.WithTimeout(context.Background(), time.Second)
defer cancel()
defer cancelWatchChanges()
cleanupExampleOrg()
defer cleanupExampleOrg()
w := Watcher{}
cc, ec := w.Listen(ctx, "does-not-exists", "keyfullchain.pem", 80*time.Millisecond)
gotChanges, gotErrors := watchChanges(ctxWatchChanges, cc, ec)
if len(gotChanges) != 0 {
t.Errorf("Expected to have no changes, found %v", gotChanges)
}
if len(gotErrors) == 0 || !os.IsNotExist(gotErrors[0]) {
t.Errorf("Expected to find errors because the watched directory doesn't exists, got %v instead", gotErrors)
}
}
func watchChanges(ctx context.Context, cc chan Change, ec chan error) ([]Change, []error) {
var changes = []Change{}
var errors = []error{}
for {
select {
case c := <-cc:
changes = append(changes, c)
case err := <-ec:
errors = append(errors, err)
if err == context.DeadlineExceeded {
return changes, errors
}
case <-ctx.Done():
panic("something went wrong with the watch timeout")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment