Skip to content

Commit

Permalink
Merge pull request #115 from adnanh/development
Browse files Browse the repository at this point in the history
Support loading hooks from multiple files
  • Loading branch information
adnanh authored Feb 11, 2017
2 parents c51971f + c8a8334 commit 8803239
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 46 deletions.
32 changes: 31 additions & 1 deletion hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ func (h *ResponseHeaders) String() string {
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
Expand All @@ -288,6 +288,23 @@ func (h *ResponseHeaders) Set(value string) error {
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
type Hook struct {
ID string `json:"id,omitempty"`
Expand Down Expand Up @@ -427,6 +444,19 @@ func (h *Hooks) LoadFromFile(path string) error {
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,
// if no hook matches the given ID, nil is returned
func (h *Hooks) Match(id string) *Hook {
Expand Down
2 changes: 1 addition & 1 deletion signals.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func watchForSignals() {
if sig == syscall.SIGUSR1 {
log.Println("caught USR1 signal")

reloadHooks()
reloadAllHooks()
} else {
log.Printf("caught unhandled signal %+v\n", sig)
}
Expand Down
179 changes: 135 additions & 44 deletions webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
)

const (
version = "2.6.1"
version = "2.6.2"
)

var (
Expand All @@ -30,24 +30,42 @@ var (
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")
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)")
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")

responseHeaders hook.ResponseHeaders
hooksFiles hook.HooksFiles

loadedHooksFromFiles = make(map[string]hook.Hooks)

watcher *fsnotify.Watcher
signals chan os.Signal

hooks hook.Hooks
)

func main() {
hooks = hook.Hooks{}
func matchLoadedHook(id string) *hook.Hook {
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.Parse()
Expand All @@ -57,6 +75,10 @@ func main() {
os.Exit(0)
}

if len(hooksFiles) == 0 {
hooksFiles = append(hooksFiles, "hooks.json")
}

log.SetPrefix("[webhook] ")
log.SetFlags(log.Ldate | log.Ltime)

Expand All @@ -70,50 +92,63 @@ func main() {
setupSignals()

// 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)

if err != nil {
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)
}
newHooks := hook.Hooks{}

log.Printf("couldn't load hooks from file! %+v\n", err)
} else {
seenHooksIds := make(map[string]bool)
err := newHooks.LoadFromFile(hooksFilePath)

log.Printf("found %d hook(s) in file\n", len(hooks))
if err != nil {
log.Printf("couldn't load hooks from file! %+v\n", err)
} else {
log.Printf("found %d hook(s) in file\n", len(newHooks))

for _, hook := range hooks {
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)
for _, hook := range newHooks {
if matchLoadedHook(hook.ID) != nil {
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 {
// set up file watcher
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
newHooksFiles := hooksFiles[:0]
for _, filePath := range hooksFiles {
if _, ok := loadedHooksFromFiles[filePath]; ok == true {
newHooksFiles = append(newHooksFiles, filePath)
}
}

hooksFiles = newHooksFiles

if !*verbose && !*noPanic && lenLoadedHooks() == 0 {
log.SetOutput(os.Stdout)
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

watcher, err = fsnotify.NewWatcher()
if err != nil {
log.Fatal("error creating file watcher instance", err)
log.Fatal("error creating file watcher instance\n", err)
}

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)
if err != nil {
log.Fatal("error adding hooks file to the watcher", err)
err = watcher.Add(hooksFilePath)
if err != nil {
log.Fatal("error adding hooks file to the watcher\n", err)
}
}

go watchForFileChange()
}

l := negroni.NewLogger()
Expand Down Expand Up @@ -159,7 +194,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {

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)

body, err := ioutil.ReadAll(r.Body)
Expand Down Expand Up @@ -302,32 +337,74 @@ func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, b
return string(out), err
}

func reloadHooks() {
newHooks := hook.Hooks{}
func reloadHooks(hooksFilePath string) {
hooksInFile := hook.Hooks{}

// 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 {
log.Printf("couldn't load hooks from file! %+v\n", err)
} else {
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 {
if seenHooksIds[hook.ID] == true {
for _, hook := range hooksInFile {
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.Println("reverting hooks back to the previous configuration")
return
}

seenHooksIds[hook.ID] = true
log.Printf("\tloaded: %s\n", hook.ID)
}

hooks = newHooks
loadedHooksFromFiles[hooksFilePath] = hooksInFile
}
}

func reloadAllHooks() {
for _, hooksFilePath := range hooksFiles {
reloadHooks(hooksFilePath)
}
}

func removeHooks(hooksFilePath string) {
for _, hook := range loadedHooksFromFiles[hooksFilePath] {
log.Printf("\tremoving: %s\n", hook.ID)
}

newHooksFiles := hooksFiles[:0]
for _, filePath := range hooksFiles {
if filePath != hooksFilePath {
newHooksFiles = append(newHooksFiles, filePath)
}
}

hooksFiles = newHooksFiles

removedHooksCount := len(loadedHooksFromFiles[hooksFilePath])

delete(loadedHooksFromFiles, hooksFilePath)

log.Printf("removed %d hook(s) that were loaded from file %s\n", removedHooksCount, hooksFilePath)

if !*verbose && !*noPanic && lenLoadedHooks() == 0 {
log.SetOutput(os.Stdout)
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 run without the hooks, either use -verbose flag, or -nopanic")
}
}

Expand All @@ -336,9 +413,23 @@ func watchForFileChange() {
select {
case event := <-(*watcher).Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("hooks file modified")

reloadHooks()
log.Printf("hooks file %s modified\n", event.Name)
reloadHooks(event.Name)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
log.Printf("hooks file %s removed, no longer watching this file for changes, removing hooks that were loaded from it\n", event.Name)
(*watcher).Remove(event.Name)
removeHooks(event.Name)
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
if _, err := os.Stat(event.Name); os.IsNotExist(err) {
// file was removed
log.Printf("hooks file %s removed, no longer watching this file for changes, and removing hooks that were loaded from it\n", event.Name)
(*watcher).Remove(event.Name)
removeHooks(event.Name)
} else {
// file was overwritten
log.Printf("hooks file %s overwritten\n", event.Name)
reloadHooks(event.Name)
}
}
case err := <-(*watcher).Errors:
log.Println("watcher error:", err)
Expand Down

0 comments on commit 8803239

Please sign in to comment.