Started work on multi file hooks loading

This commit is contained in:
Adnan Hajdarevic 2016-12-05 21:22:34 +01:00
parent 07f166616c
commit 8207c6cf12
3 changed files with 116 additions and 45 deletions

View file

@ -273,7 +273,7 @@ func (h *ResponseHeaders) String() string {
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value) result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
} }
return fmt.Sprint(strings.Join(result, ", ")) return strings.Join(result, ", ")
} }
// Set method appends new Header object from header=value notation // Set method appends new Header object from header=value notation
@ -288,6 +288,23 @@ func (h *ResponseHeaders) Set(value string) error {
return nil return nil
} }
// HooksFiles is a slice of String
type HooksFiles []string
func (h *HooksFiles) String() string {
if len(*h) == 0 {
return "hooks.json"
}
return strings.Join(*h, ", ")
}
// Set method appends new string
func (h *HooksFiles) Set(value string) error {
*h = append(*h, value)
return nil
}
// Hook type is a structure containing details for a single hook // Hook type is a structure containing details for a single hook
type Hook struct { type Hook struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
@ -426,6 +443,19 @@ func (h *Hooks) LoadFromFile(path string) error {
return e return e
} }
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
func (h *Hooks) Append(other *Hooks) error {
for _, hook := range *other {
if h.Match(hook.ID) != nil {
return fmt.Errorf("hook with ID %s is already defined", hook.ID)
}
*h = append(*h, hook)
}
return nil
}
// Match iterates through Hooks and returns first one that matches the given ID, // Match iterates through Hooks and returns first one that matches the given ID,
// if no hook matches the given ID, nil is returned // if no hook matches the given ID, nil is returned
func (h *Hooks) Match(id string) *Hook { func (h *Hooks) Match(id string) *Hook {

View file

@ -26,7 +26,7 @@ func watchForSignals() {
if sig == syscall.SIGUSR1 { if sig == syscall.SIGUSR1 {
log.Println("caught USR1 signal") log.Println("caught USR1 signal")
reloadHooks() reloadAllHooks()
} else { } else {
log.Printf("caught unhandled signal %+v\n", sig) log.Printf("caught unhandled signal %+v\n", sig)
} }

View file

@ -21,7 +21,7 @@ import (
) )
const ( const (
version = "2.6.0" version = "2.6.1"
) )
var ( var (
@ -30,7 +30,6 @@ var (
verbose = flag.Bool("verbose", false, "show verbose output") verbose = flag.Bool("verbose", false, "show verbose output")
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode") noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically") hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve")
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)") hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP") secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file") cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
@ -38,20 +37,43 @@ var (
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit") justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
responseHeaders hook.ResponseHeaders responseHeaders hook.ResponseHeaders
hooksFiles hook.HooksFiles
loadedHooksFromFiles = make(map[string]hook.Hooks)
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
signals chan os.Signal signals chan os.Signal
hooks hook.Hooks
) )
func main() { func matchLoadedHook(id string) *hook.Hook {
hooks = hook.Hooks{} for _, hooks := range loadedHooksFromFiles {
if hook := hooks.Match(id); hook != nil {
return hook
}
}
return nil
}
func lenLoadedHooks() int {
sum := 0
for _, hooks := range loadedHooksFromFiles {
sum += len(hooks)
}
return sum
}
func main() {
flag.Var(&hooksFiles, "hooks", "path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files")
flag.Var(&responseHeaders, "header", "response header to return, specified in format name=value, use multiple times to set multiple headers") flag.Var(&responseHeaders, "header", "response header to return, specified in format name=value, use multiple times to set multiple headers")
flag.Parse() flag.Parse()
if len(hooksFiles) == 0 {
log.Fatalln("you must specify at least one file to load hooks from")
}
if *justDisplayVersion { if *justDisplayVersion {
fmt.Println("webhook version " + version) fmt.Println("webhook version " + version)
os.Exit(0) os.Exit(0)
@ -70,50 +92,54 @@ func main() {
setupSignals() setupSignals()
// load and parse hooks // load and parse hooks
log.Printf("attempting to load hooks from %s\n", *hooksFilePath) for _, hooksFilePath := range hooksFiles {
log.Printf("attempting to load hooks from %s\n", hooksFilePath)
err := hooks.LoadFromFile(*hooksFilePath) newHooks := hook.Hooks{}
if err != nil { err := newHooks.LoadFromFile(hooksFilePath)
if !*verbose && !*noPanic {
log.SetOutput(os.Stdout)
log.Fatalf("couldn't load any hooks from file! %+v\naborting webhook execution since the -verbose flag is set to false.\nIf, for some reason, you want webhook to start without the hooks, either use -verbose flag, or -nopanic", err)
}
log.Printf("couldn't load hooks from file! %+v\n", err) if err != nil {
} else { log.Printf("couldn't load hooks from file! %+v\n", err)
seenHooksIds := make(map[string]bool) } else {
log.Printf("found %d hook(s) in file\n", len(newHooks))
log.Printf("found %d hook(s) in file\n", len(hooks)) for _, hook := range newHooks {
if matchLoadedHook(hook.ID) != nil {
for _, hook := range hooks { log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID)
if seenHooksIds[hook.ID] == true { }
log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID) log.Printf("\tloaded: %s\n", hook.ID)
} }
seenHooksIds[hook.ID] = true
log.Printf("\tloaded: %s\n", hook.ID) loadedHooksFromFiles[hooksFilePath] = newHooks
} }
} }
if *hotReload { if !*verbose && !*noPanic && lenLoadedHooks() == 0 {
// set up file watcher log.SetOutput(os.Stdout)
log.Printf("setting up file watcher for %s\n", *hooksFilePath) log.Fatalln("couldn't load any hooks from file!\naborting webhook execution since the -verbose flag is set to false.\nIf, for some reason, you want webhook to start without the hooks, either use -verbose flag, or -nopanic")
}
if *hotReload {
var err error var err error
watcher, err = fsnotify.NewWatcher() watcher, err = fsnotify.NewWatcher()
if err != nil { if err != nil {
log.Fatal("error creating file watcher instance", err) log.Fatal("error creating file watcher instance", err)
} }
defer watcher.Close() defer watcher.Close()
go watchForFileChange() for _, hooksFilePath := range hooksFiles {
// set up file watcher
log.Printf("setting up file watcher for %s\n", hooksFilePath)
err = watcher.Add(*hooksFilePath) err = watcher.Add(hooksFilePath)
if err != nil { if err != nil {
log.Fatal("error adding hooks file to the watcher", err) log.Fatal("error adding hooks file to the watcher", err)
}
} }
go watchForFileChange()
} }
l := negroni.NewLogger() l := negroni.NewLogger()
@ -159,7 +185,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"] id := mux.Vars(r)["id"]
if matchedHook := hooks.Match(id); matchedHook != nil { if matchedHook := matchLoadedHook(id); matchedHook != nil {
log.Printf("%s got matched\n", id) log.Printf("%s got matched\n", id)
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
@ -291,32 +317,48 @@ func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, b
return string(out), err return string(out), err
} }
func reloadHooks() { func reloadHooks(hooksFilePath string) {
newHooks := hook.Hooks{} hooksInFile := hook.Hooks{}
// parse and swap // parse and swap
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath) log.Printf("attempting to reload hooks from %s\n", hooksFilePath)
err := newHooks.LoadFromFile(*hooksFilePath) err := hooksInFile.LoadFromFile(hooksFilePath)
if err != nil { if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err) log.Printf("couldn't load hooks from file! %+v\n", err)
} else { } else {
seenHooksIds := make(map[string]bool) seenHooksIds := make(map[string]bool)
log.Printf("found %d hook(s) in file\n", len(newHooks)) log.Printf("found %d hook(s) in file\n", len(hooksInFile))
for _, hook := range newHooks { for _, hook := range hooksInFile {
if seenHooksIds[hook.ID] == true { wasHookIDAlreadyLoaded := false
for _, loadedHook := range loadedHooksFromFiles[hooksFilePath] {
if loadedHook.ID == hook.ID {
wasHookIDAlreadyLoaded = true
break
}
}
if (matchLoadedHook(hook.ID) != nil && !wasHookIDAlreadyLoaded) || seenHooksIds[hook.ID] == true {
log.Printf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!", hook.ID) log.Printf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!", hook.ID)
log.Println("reverting hooks back to the previous configuration") log.Println("reverting hooks back to the previous configuration")
return return
} }
seenHooksIds[hook.ID] = true seenHooksIds[hook.ID] = true
log.Printf("\tloaded: %s\n", hook.ID) log.Printf("\tloaded: %s\n", hook.ID)
} }
hooks = newHooks loadedHooksFromFiles[hooksFilePath] = hooksInFile
}
}
func reloadAllHooks() {
for _, hooksFilePath := range hooksFiles {
reloadHooks(hooksFilePath)
} }
} }
@ -325,9 +367,8 @@ func watchForFileChange() {
select { select {
case event := <-(*watcher).Events: case event := <-(*watcher).Events:
if event.Op&fsnotify.Write == fsnotify.Write { if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("hooks file modified") log.Printf("hooks file %s modified\n", event.Name)
reloadHooks(event.Name)
reloadHooks()
} }
case err := <-(*watcher).Errors: case err := <-(*watcher).Errors:
log.Println("watcher error:", err) log.Println("watcher error:", err)