From f5f04ddaa24d0eb4c903b1e7598578a625d0868a Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Tue, 15 Aug 2017 18:02:33 -0500 Subject: [PATCH] Allow hooks file to be parsed as a template Add a -template command line option that instructs webhook to parse the hooks files as Go text templates. Includes a `getenv` template func for retrieving environment variables. --- hook/hook.go | 31 +++++++++++++++++++-- hook/hook_test.go | 44 +++++++++++++++++++++++++----- hooks.json.tmpl.example | 60 +++++++++++++++++++++++++++++++++++++++++ hooks.yaml.tmpl.example | 28 +++++++++++++++++++ webhook.go | 5 ++-- 5 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 hooks.json.tmpl.example create mode 100644 hooks.yaml.tmpl.example diff --git a/hook/hook.go b/hook/hook.go index 47ae7dd..b4eae95 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -1,6 +1,7 @@ package hook import ( + "bytes" "crypto/hmac" "crypto/sha1" "crypto/sha256" @@ -18,6 +19,7 @@ import ( "regexp" "strconv" "strings" + "text/template" "github.com/ghodss/yaml" ) @@ -544,8 +546,10 @@ func (h *Hook) ExtractCommandArgumentsForFile(headers, query, payload *map[strin // Hooks is an array of Hook objects type Hooks []Hook -// LoadFromFile attempts to load hooks from specified JSON file -func (h *Hooks) LoadFromFile(path string) error { +// LoadFromFile attempts to load hooks from the specified file, which +// can be either JSON or YAML. The asTemplate parameter causes the file +// contents to be parsed as a Go text/template prior to unmarshalling. +func (h *Hooks) LoadFromFile(path string, asTemplate bool) error { if path == "" { return nil } @@ -557,6 +561,24 @@ func (h *Hooks) LoadFromFile(path string) error { return e } + if asTemplate { + funcMap := template.FuncMap{"getenv": getenv} + + tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file)) + if err != nil { + return err + } + + var buf bytes.Buffer + + err = tmpl.Execute(&buf, nil) + if err != nil { + return err + } + + file = buf.Bytes() + } + e = yaml.Unmarshal(file, h) return e } @@ -705,3 +727,8 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod } return false, nil } + +// getenv provides a template function to retrieve OS environment variables. +func getenv(s string) string { + return os.Getenv(s) +} diff --git a/hook/hook_test.go b/hook/hook_test.go index 98be216..52f273b 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -1,6 +1,7 @@ package hook import ( + "os" "reflect" "strings" "testing" @@ -229,26 +230,55 @@ func TestHookExtractCommandArgumentsForEnv(t *testing.T) { } var hooksLoadFromFileTests = []struct { - path string - ok bool + path string + asTemplate bool + ok bool }{ - {"../hooks.json.example", true}, - {"../hooks.yaml.example", true}, - {"", true}, + {"../hooks.json.example", false, true}, + {"../hooks.yaml.example", false, true}, + {"../hooks.json.tmpl.example", true, true}, + {"../hooks.yaml.tmpl.example", true, true}, + {"", false, true}, // failures - {"missing.json", false}, + {"missing.json", false, false}, } func TestHooksLoadFromFile(t *testing.T) { + secret := `foo"123` + os.Setenv("XXXTEST_SECRET", secret) + for _, tt := range hooksLoadFromFileTests { h := &Hooks{} - err := h.LoadFromFile(tt.path) + err := h.LoadFromFile(tt.path, tt.asTemplate) if (err == nil) != tt.ok { t.Errorf(err.Error()) } } } +func TestHooksTemplateLoadFromFile(t *testing.T) { + secret := `foo"123` + os.Setenv("XXXTEST_SECRET", secret) + + for _, tt := range hooksLoadFromFileTests { + if !tt.asTemplate { + continue + } + + h := &Hooks{} + err := h.LoadFromFile(tt.path, tt.asTemplate) + if (err == nil) != tt.ok { + t.Errorf(err.Error()) + continue + } + + s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret + if s != secret { + t.Errorf("Expected secret of %q, got %q", secret, s) + } + } +} + var hooksMatchTests = []struct { id string hooks Hooks diff --git a/hooks.json.tmpl.example b/hooks.json.tmpl.example new file mode 100644 index 0000000..70e05c8 --- /dev/null +++ b/hooks.json.tmpl.example @@ -0,0 +1,60 @@ +[ + { + "id": "webhook", + "execute-command": "/home/adnan/redeploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "response-message": "I got the payload!", + "response-headers": + [ + { + "name": "Access-Control-Allow-Origin", + "value": "*" + } + ], + "pass-arguments-to-command": + [ + { + "source": "payload", + "name": "head_commit.id" + }, + { + "source": "payload", + "name": "pusher.name" + }, + { + "source": "payload", + "name": "pusher.email" + } + ], + "trigger-rule": + { + "and": + [ + { + "match": + { + "type": "payload-hash-sha1", + "secret": "{{ getenv "XXXTEST_SECRET" | js }}", + "parameter": + { + "source": "header", + "name": "X-Hub-Signature" + } + } + }, + { + "match": + { + "type": "value", + "value": "refs/heads/master", + "parameter": + { + "source": "payload", + "name": "ref" + } + } + } + ] + } + } +] diff --git a/hooks.yaml.tmpl.example b/hooks.yaml.tmpl.example new file mode 100644 index 0000000..2bcfbd6 --- /dev/null +++ b/hooks.yaml.tmpl.example @@ -0,0 +1,28 @@ +- id: webhook + execute-command: /home/adnan/redeploy-go-webhook.sh + command-working-directory: /home/adnan/go + response-message: I got the payload! + response-headers: + - name: Access-Control-Allow-Origin + value: '*' + pass-arguments-to-command: + - source: payload + name: head_commit.id + - source: payload + name: pusher.name + - source: payload + name: pusher.email + trigger-rule: + and: + - match: + type: payload-hash-sha1 + secret: "{{ getenv "XXXTEST_SECRET" | js }}" + parameter: + source: header + name: X-Hub-Signature + - match: + type: value + value: refs/heads/master + parameter: + source: payload + name: ref diff --git a/webhook.go b/webhook.go index 41b02b1..4df45f8 100644 --- a/webhook.go +++ b/webhook.go @@ -34,6 +34,7 @@ var ( hotReload = flag.Bool("hotreload", false, "watch hooks file for changes and reload them automatically") 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") + asTemplate = flag.Bool("template", false, "parse hooks file as a Go template") 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") @@ -99,7 +100,7 @@ func main() { newHooks := hook.Hooks{} - err := newHooks.LoadFromFile(hooksFilePath) + err := newHooks.LoadFromFile(hooksFilePath, *asTemplate) if err != nil { log.Printf("couldn't load hooks from file! %+v\n", err) @@ -407,7 +408,7 @@ func reloadHooks(hooksFilePath string) { // parse and swap log.Printf("attempting to reload hooks from %s\n", hooksFilePath) - err := hooksInFile.LoadFromFile(hooksFilePath) + err := hooksInFile.LoadFromFile(hooksFilePath, *asTemplate) if err != nil { log.Printf("couldn't load hooks from file! %+v\n", err)