Skip to content

Commit 8803239

Browse files
authored
Merge pull request #115 from adnanh/development
Support loading hooks from multiple files
2 parents c51971f + c8a8334 commit 8803239

File tree

3 files changed

+167
-46
lines changed

3 files changed

+167
-46
lines changed

hook/hook.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ func (h *ResponseHeaders) String() string {
273273
result[idx] = fmt.Sprintf("%s=%s", responseHeader.Name, responseHeader.Value)
274274
}
275275

276-
return fmt.Sprint(strings.Join(result, ", "))
276+
return strings.Join(result, ", ")
277277
}
278278

279279
// Set method appends new Header object from header=value notation
@@ -288,6 +288,23 @@ func (h *ResponseHeaders) Set(value string) error {
288288
return nil
289289
}
290290

291+
// HooksFiles is a slice of String
292+
type HooksFiles []string
293+
294+
func (h *HooksFiles) String() string {
295+
if len(*h) == 0 {
296+
return "hooks.json"
297+
}
298+
299+
return strings.Join(*h, ", ")
300+
}
301+
302+
// Set method appends new string
303+
func (h *HooksFiles) Set(value string) error {
304+
*h = append(*h, value)
305+
return nil
306+
}
307+
291308
// Hook type is a structure containing details for a single hook
292309
type Hook struct {
293310
ID string `json:"id,omitempty"`
@@ -427,6 +444,19 @@ func (h *Hooks) LoadFromFile(path string) error {
427444
return e
428445
}
429446

447+
// Append appends hooks unless the new hooks contain a hook with an ID that already exists
448+
func (h *Hooks) Append(other *Hooks) error {
449+
for _, hook := range *other {
450+
if h.Match(hook.ID) != nil {
451+
return fmt.Errorf("hook with ID %s is already defined", hook.ID)
452+
}
453+
454+
*h = append(*h, hook)
455+
}
456+
457+
return nil
458+
}
459+
430460
// Match iterates through Hooks and returns first one that matches the given ID,
431461
// if no hook matches the given ID, nil is returned
432462
func (h *Hooks) Match(id string) *Hook {

signals.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func watchForSignals() {
2626
if sig == syscall.SIGUSR1 {
2727
log.Println("caught USR1 signal")
2828

29-
reloadHooks()
29+
reloadAllHooks()
3030
} else {
3131
log.Printf("caught unhandled signal %+v\n", sig)
3232
}

webhook.go

Lines changed: 135 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
)
2222

2323
const (
24-
version = "2.6.1"
24+
version = "2.6.2"
2525
)
2626

2727
var (
@@ -30,24 +30,42 @@ var (
3030
verbose = flag.Bool("verbose", false, "show verbose output")
3131
noPanic = flag.Bool("nopanic", false, "do not panic if hooks cannot be loaded when webhook is not running in verbose mode")
3232
hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically")
33-
hooksFilePath = flag.String("hooks", "hooks.json", "path to the json file containing defined hooks the webhook should serve")
3433
hooksURLPrefix = flag.String("urlprefix", "hooks", "url prefix to use for served hooks (protocol://yourserver:port/PREFIX/:hook-id)")
3534
secure = flag.Bool("secure", false, "use HTTPS instead of HTTP")
3635
cert = flag.String("cert", "cert.pem", "path to the HTTPS certificate pem file")
3736
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key pem file")
3837
justDisplayVersion = flag.Bool("version", false, "display webhook version and quit")
3938

4039
responseHeaders hook.ResponseHeaders
40+
hooksFiles hook.HooksFiles
41+
42+
loadedHooksFromFiles = make(map[string]hook.Hooks)
4143

4244
watcher *fsnotify.Watcher
4345
signals chan os.Signal
44-
45-
hooks hook.Hooks
4646
)
4747

48-
func main() {
49-
hooks = hook.Hooks{}
48+
func matchLoadedHook(id string) *hook.Hook {
49+
for _, hooks := range loadedHooksFromFiles {
50+
if hook := hooks.Match(id); hook != nil {
51+
return hook
52+
}
53+
}
54+
55+
return nil
56+
}
57+
58+
func lenLoadedHooks() int {
59+
sum := 0
60+
for _, hooks := range loadedHooksFromFiles {
61+
sum += len(hooks)
62+
}
63+
64+
return sum
65+
}
5066

67+
func main() {
68+
flag.Var(&hooksFiles, "hooks", "path to the json file containing defined hooks the webhook should serve, use multiple times to load from different files")
5169
flag.Var(&responseHeaders, "header", "response header to return, specified in format name=value, use multiple times to set multiple headers")
5270

5371
flag.Parse()
@@ -57,6 +75,10 @@ func main() {
5775
os.Exit(0)
5876
}
5977

78+
if len(hooksFiles) == 0 {
79+
hooksFiles = append(hooksFiles, "hooks.json")
80+
}
81+
6082
log.SetPrefix("[webhook] ")
6183
log.SetFlags(log.Ldate | log.Ltime)
6284

@@ -70,50 +92,63 @@ func main() {
7092
setupSignals()
7193

7294
// load and parse hooks
73-
log.Printf("attempting to load hooks from %s\n", *hooksFilePath)
95+
for _, hooksFilePath := range hooksFiles {
96+
log.Printf("attempting to load hooks from %s\n", hooksFilePath)
7497

75-
err := hooks.LoadFromFile(*hooksFilePath)
76-
77-
if err != nil {
78-
if !*verbose && !*noPanic {
79-
log.SetOutput(os.Stdout)
80-
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)
81-
}
98+
newHooks := hook.Hooks{}
8299

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

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

89-
for _, hook := range hooks {
90-
if seenHooksIds[hook.ID] == true {
91-
log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID)
107+
for _, hook := range newHooks {
108+
if matchLoadedHook(hook.ID) != nil {
109+
log.Fatalf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!\n", hook.ID)
110+
}
111+
log.Printf("\tloaded: %s\n", hook.ID)
92112
}
93-
seenHooksIds[hook.ID] = true
94-
log.Printf("\tloaded: %s\n", hook.ID)
113+
114+
loadedHooksFromFiles[hooksFilePath] = newHooks
95115
}
96116
}
97117

98-
if *hotReload {
99-
// set up file watcher
100-
log.Printf("setting up file watcher for %s\n", *hooksFilePath)
118+
newHooksFiles := hooksFiles[:0]
119+
for _, filePath := range hooksFiles {
120+
if _, ok := loadedHooksFromFiles[filePath]; ok == true {
121+
newHooksFiles = append(newHooksFiles, filePath)
122+
}
123+
}
124+
125+
hooksFiles = newHooksFiles
126+
127+
if !*verbose && !*noPanic && lenLoadedHooks() == 0 {
128+
log.SetOutput(os.Stdout)
129+
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")
130+
}
101131

132+
if *hotReload {
102133
var err error
103134

104135
watcher, err = fsnotify.NewWatcher()
105136
if err != nil {
106-
log.Fatal("error creating file watcher instance", err)
137+
log.Fatal("error creating file watcher instance\n", err)
107138
}
108-
109139
defer watcher.Close()
110140

111-
go watchForFileChange()
141+
for _, hooksFilePath := range hooksFiles {
142+
// set up file watcher
143+
log.Printf("setting up file watcher for %s\n", hooksFilePath)
112144

113-
err = watcher.Add(*hooksFilePath)
114-
if err != nil {
115-
log.Fatal("error adding hooks file to the watcher", err)
145+
err = watcher.Add(hooksFilePath)
146+
if err != nil {
147+
log.Fatal("error adding hooks file to the watcher\n", err)
148+
}
116149
}
150+
151+
go watchForFileChange()
117152
}
118153

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

160195
id := mux.Vars(r)["id"]
161196

162-
if matchedHook := hooks.Match(id); matchedHook != nil {
197+
if matchedHook := matchLoadedHook(id); matchedHook != nil {
163198
log.Printf("%s got matched\n", id)
164199

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

305-
func reloadHooks() {
306-
newHooks := hook.Hooks{}
340+
func reloadHooks(hooksFilePath string) {
341+
hooksInFile := hook.Hooks{}
307342

308343
// parse and swap
309-
log.Printf("attempting to reload hooks from %s\n", *hooksFilePath)
344+
log.Printf("attempting to reload hooks from %s\n", hooksFilePath)
310345

311-
err := newHooks.LoadFromFile(*hooksFilePath)
346+
err := hooksInFile.LoadFromFile(hooksFilePath)
312347

313348
if err != nil {
314349
log.Printf("couldn't load hooks from file! %+v\n", err)
315350
} else {
316351
seenHooksIds := make(map[string]bool)
317352

318-
log.Printf("found %d hook(s) in file\n", len(newHooks))
353+
log.Printf("found %d hook(s) in file\n", len(hooksInFile))
319354

320-
for _, hook := range newHooks {
321-
if seenHooksIds[hook.ID] == true {
355+
for _, hook := range hooksInFile {
356+
wasHookIDAlreadyLoaded := false
357+
358+
for _, loadedHook := range loadedHooksFromFiles[hooksFilePath] {
359+
if loadedHook.ID == hook.ID {
360+
wasHookIDAlreadyLoaded = true
361+
break
362+
}
363+
}
364+
365+
if (matchLoadedHook(hook.ID) != nil && !wasHookIDAlreadyLoaded) || seenHooksIds[hook.ID] == true {
322366
log.Printf("error: hook with the id %s has already been loaded!\nplease check your hooks file for duplicate hooks ids!", hook.ID)
323367
log.Println("reverting hooks back to the previous configuration")
324368
return
325369
}
370+
326371
seenHooksIds[hook.ID] = true
327372
log.Printf("\tloaded: %s\n", hook.ID)
328373
}
329374

330-
hooks = newHooks
375+
loadedHooksFromFiles[hooksFilePath] = hooksInFile
376+
}
377+
}
378+
379+
func reloadAllHooks() {
380+
for _, hooksFilePath := range hooksFiles {
381+
reloadHooks(hooksFilePath)
382+
}
383+
}
384+
385+
func removeHooks(hooksFilePath string) {
386+
for _, hook := range loadedHooksFromFiles[hooksFilePath] {
387+
log.Printf("\tremoving: %s\n", hook.ID)
388+
}
389+
390+
newHooksFiles := hooksFiles[:0]
391+
for _, filePath := range hooksFiles {
392+
if filePath != hooksFilePath {
393+
newHooksFiles = append(newHooksFiles, filePath)
394+
}
395+
}
396+
397+
hooksFiles = newHooksFiles
398+
399+
removedHooksCount := len(loadedHooksFromFiles[hooksFilePath])
400+
401+
delete(loadedHooksFromFiles, hooksFilePath)
402+
403+
log.Printf("removed %d hook(s) that were loaded from file %s\n", removedHooksCount, hooksFilePath)
404+
405+
if !*verbose && !*noPanic && lenLoadedHooks() == 0 {
406+
log.SetOutput(os.Stdout)
407+
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")
331408
}
332409
}
333410

@@ -336,9 +413,23 @@ func watchForFileChange() {
336413
select {
337414
case event := <-(*watcher).Events:
338415
if event.Op&fsnotify.Write == fsnotify.Write {
339-
log.Println("hooks file modified")
340-
341-
reloadHooks()
416+
log.Printf("hooks file %s modified\n", event.Name)
417+
reloadHooks(event.Name)
418+
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
419+
log.Printf("hooks file %s removed, no longer watching this file for changes, removing hooks that were loaded from it\n", event.Name)
420+
(*watcher).Remove(event.Name)
421+
removeHooks(event.Name)
422+
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
423+
if _, err := os.Stat(event.Name); os.IsNotExist(err) {
424+
// file was removed
425+
log.Printf("hooks file %s removed, no longer watching this file for changes, and removing hooks that were loaded from it\n", event.Name)
426+
(*watcher).Remove(event.Name)
427+
removeHooks(event.Name)
428+
} else {
429+
// file was overwritten
430+
log.Printf("hooks file %s overwritten\n", event.Name)
431+
reloadHooks(event.Name)
432+
}
342433
}
343434
case err := <-(*watcher).Errors:
344435
log.Println("watcher error:", err)

0 commit comments

Comments
 (0)