- mocks/certs/example.com/keyfullchain.pem
- mocks/certs/example.net/keyfullchain.pem
Last active
April 5, 2019 16:25
-
-
Save henvic/4da7dc2fac692388d0d5c0747aa54e7d to your computer and use it in GitHub Desktop.
certlistener (old)
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
mocks/certs/example.org |
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
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, | |
} | |
} |
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
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