mirror of
https://github.com/adnanh/webhook.git
synced 2025-05-10 15:44:43 +00:00
Add environment arguments and improve testing
There's a lot in this commit. 1. Add `pass-environment-to-command` option that works much like `pass-arguments-to-command`. You can see an example usage in the "github" test case. 2. Add a test program called "hookecho" that is used to test the webhook package instead of relying upon a system `echo` command. 3. Move hooks_test.json to a template so that we can update the path to hookecho on the fly. 4. Don't return an error at the end of hook.MatchRule.Evaluate(). All tests succeed for me now.
This commit is contained in:
parent
6774079a57
commit
ea3dbf3438
7 changed files with 167 additions and 32 deletions
|
@ -11,7 +11,8 @@ If you use Slack, you can set up an "Outgoing webhook integration" to run variou
|
|||
1. receive the request,
|
||||
2. parse the headers, payload and query variables,
|
||||
3. check if the specified rules for the hook are satisfied,
|
||||
3. and finally, pass the specified arguments to the specified command.
|
||||
3. and finally, pass the specified arguments to the specified command via
|
||||
command line arguments or via environment variables.
|
||||
|
||||
Everything else is the responsibility of the command's author.
|
||||
|
||||
|
|
43
hook/hook.go
43
hook/hook.go
|
@ -25,6 +25,12 @@ const (
|
|||
SourceEntireHeaders string = "entire-headers"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvNamespace is the prefix used for passing arguments into the command
|
||||
// environment.
|
||||
EnvNamespace string = "HOOK_"
|
||||
)
|
||||
|
||||
// ErrInvalidPayloadSignature describes an invalid payload signature.
|
||||
var ErrInvalidPayloadSignature = errors.New("invalid payload signature")
|
||||
|
||||
|
@ -234,14 +240,15 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
|
|||
|
||||
// Hook type is a structure containing details for a single hook
|
||||
type Hook struct {
|
||||
ID string `json:"id"`
|
||||
ExecuteCommand string `json:"execute-command"`
|
||||
CommandWorkingDirectory string `json:"command-working-directory"`
|
||||
ResponseMessage string `json:"response-message"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
|
||||
TriggerRule *Rules `json:"trigger-rule"`
|
||||
ID string `json:"id"`
|
||||
ExecuteCommand string `json:"execute-command"`
|
||||
CommandWorkingDirectory string `json:"command-working-directory"`
|
||||
ResponseMessage string `json:"response-message"`
|
||||
CaptureCommandOutput bool `json:"include-command-output-in-response"`
|
||||
PassEnvironmentToCommand []Argument `json:"pass-environment-to-command"`
|
||||
PassArgumentsToCommand []Argument `json:"pass-arguments-to-command"`
|
||||
JSONStringParameters []Argument `json:"parse-parameters-as-json"`
|
||||
TriggerRule *Rules `json:"trigger-rule"`
|
||||
}
|
||||
|
||||
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the
|
||||
|
@ -295,7 +302,6 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
|||
if arg, ok := h.PassArgumentsToCommand[i].Get(headers, query, payload); ok {
|
||||
args = append(args, arg)
|
||||
} else {
|
||||
args = append(args, "")
|
||||
return args, &ArgumentError{h.PassArgumentsToCommand[i]}
|
||||
}
|
||||
}
|
||||
|
@ -303,6 +309,23 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
|
|||
return args, nil
|
||||
}
|
||||
|
||||
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value
|
||||
// format, based on the PassEnvironmentToCommand property that is ready to be used
|
||||
// with exec.Command().
|
||||
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, error) {
|
||||
var args = make([]string, 0)
|
||||
|
||||
for i := range h.PassEnvironmentToCommand {
|
||||
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
|
||||
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
|
||||
} else {
|
||||
return args, &ArgumentError{h.PassEnvironmentToCommand[i]}
|
||||
}
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Hooks is an array of Hook objects
|
||||
type Hooks []Hook
|
||||
|
||||
|
@ -459,7 +482,7 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
|
|||
return err == nil, err
|
||||
}
|
||||
}
|
||||
return false, &ArgumentError{r.Parameter}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CommandStatusResponse type encapsulates the executed command exit code, message, stdout and stderr
|
||||
|
|
|
@ -123,7 +123,7 @@ var hookExtractCommandArgumentsTests = []struct {
|
|||
}{
|
||||
{"test", []Argument{Argument{"header", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"test", "z"}, true},
|
||||
// failures
|
||||
{"fail", []Argument{Argument{"payload", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail", ""}, false},
|
||||
{"fail", []Argument{Argument{"payload", "a"}}, &map[string]interface{}{"a": "z"}, nil, nil, []string{"fail"}, false},
|
||||
}
|
||||
|
||||
func TestHookExtractCommandArguments(t *testing.T) {
|
||||
|
@ -188,8 +188,8 @@ var matchRuleTests = []struct {
|
|||
// failures
|
||||
{"value", "", "", "X", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"regex", "^X", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, false},
|
||||
{"value", "", "2", "X", Argument{"header", "a"}, &map[string]interface{}{"y": "z"}, nil, nil, []byte{}, false, false}, // reference invalid header
|
||||
// errors
|
||||
{"value", "", "2", "X", Argument{"header", "a"}, &map[string]interface{}{"y": "z"}, nil, nil, []byte{}, false, true}, // reference invalid header
|
||||
{"regex", "*", "", "", Argument{"header", "a"}, &map[string]interface{}{"a": "z"}, nil, nil, []byte{}, false, true}, // invalid regex
|
||||
{"payload-hash-sha1", "", "secret", "", Argument{"header", "a"}, &map[string]interface{}{"a": ""}, nil, nil, []byte{}, false, true}, // invalid hmac
|
||||
}
|
||||
|
@ -262,7 +262,7 @@ var andRuleTests = []struct {
|
|||
"invalid rule",
|
||||
AndRule{{Match: &MatchRule{"value", "", "", "X", Argument{"header", "a"}}}},
|
||||
&map[string]interface{}{"y": "z"}, nil, nil, nil,
|
||||
false, true,
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -317,7 +317,7 @@ var orRuleTests = []struct {
|
|||
{Match: &MatchRule{"value", "", "", "z", Argument{"header", "a"}}},
|
||||
},
|
||||
&map[string]interface{}{"y": "Z"}, nil, nil, []byte{},
|
||||
false, true,
|
||||
false, false,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
26
test/hookecho.go
Normal file
26
test/hookecho.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Hook Echo is a simply utility used for testing the Webhook package.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
fmt.Printf("arg: %s\n", strings.Join(os.Args[1:], " "))
|
||||
}
|
||||
|
||||
var env []string
|
||||
for _, v := range os.Environ() {
|
||||
if strings.HasPrefix(v, "HOOK_") {
|
||||
env = append(env, v)
|
||||
}
|
||||
}
|
||||
|
||||
if len(env) > 0 {
|
||||
fmt.Printf("env: %s\n", strings.Join(env, " "))
|
||||
}
|
||||
}
|
|
@ -1,9 +1,16 @@
|
|||
[
|
||||
{
|
||||
"id": "github",
|
||||
"execute-command": "/bin/echo",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"pass-environment-to-command":
|
||||
[
|
||||
{
|
||||
"source": "payload",
|
||||
"name": "pusher.email"
|
||||
}
|
||||
],
|
||||
"pass-arguments-to-command":
|
||||
[
|
||||
{
|
||||
|
@ -52,7 +59,7 @@
|
|||
},
|
||||
{
|
||||
"id": "bitbucket",
|
||||
"execute-command": "/bin/echo",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"include-command-output-in-response": true,
|
||||
"response-message": "success",
|
||||
|
@ -99,7 +106,7 @@
|
|||
},
|
||||
{
|
||||
"id": "gitlab",
|
||||
"execute-command": "/bin/echo",
|
||||
"execute-command": "{{ .Hookecho }}",
|
||||
"command-working-directory": "/",
|
||||
"response-message": "success",
|
||||
"include-command-output-in-response": true,
|
||||
|
@ -133,3 +140,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -234,13 +234,20 @@ func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, b
|
|||
|
||||
cmd := exec.Command(h.ExecuteCommand)
|
||||
cmd.Dir = h.CommandWorkingDirectory
|
||||
|
||||
cmd.Args, err = h.ExtractCommandArguments(headers, query, payload)
|
||||
if err != nil {
|
||||
log.Printf("error extracting command arguments: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Printf("executing %s (%s) with arguments %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Dir)
|
||||
cmd.Env, err = h.ExtractCommandArgumentsForEnv(headers, query, payload)
|
||||
if err != nil {
|
||||
log.Printf("error extracting command arguments: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Printf("executing %s (%s) with arguments %s and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, cmd.Env, cmd.Dir)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -13,18 +11,28 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/adnanh/webhook/hook"
|
||||
)
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
bin, cleanup := buildWebhook(t)
|
||||
defer cleanup()
|
||||
hookecho, cleanupHookecho := buildHookecho(t)
|
||||
defer cleanupHookecho()
|
||||
|
||||
config, cleanupConfig := genConfig(t, hookecho)
|
||||
defer cleanupConfig()
|
||||
|
||||
webhook, cleanupWebhook := buildWebhook(t)
|
||||
defer cleanupWebhook()
|
||||
|
||||
ip, port := serverAddress(t)
|
||||
args := []string{"-hooks=hooks_test.json", fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
|
||||
args := []string{fmt.Sprintf("-hooks=%s", config), fmt.Sprintf("-ip=%s", ip), fmt.Sprintf("-port=%s", port), "-verbose"}
|
||||
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd := exec.Command(webhook, args...)
|
||||
//cmd.Stderr = os.Stderr // uncomment to see verbose output
|
||||
cmd.Env = webhookEnv()
|
||||
cmd.Args[0] = "webhook"
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start webhook: %s", err)
|
||||
|
@ -41,10 +49,8 @@ func TestWebhook(t *testing.T) {
|
|||
t.Errorf("New request failed: %s", err)
|
||||
}
|
||||
|
||||
if tt.headers != nil {
|
||||
for k, v := range tt.headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
for k, v := range tt.headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
|
||||
var res *http.Response
|
||||
|
@ -73,6 +79,58 @@ func TestWebhook(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func buildHookecho(t *testing.T) (bin string, cleanup func()) {
|
||||
tmp, err := ioutil.TempDir("", "hookecho-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanup == nil {
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
|
||||
bin = filepath.Join(tmp, "hookecho")
|
||||
if runtime.GOOS == "windows" {
|
||||
bin += ".exe"
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", bin, "test/hookecho.go")
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("Building hookecho: %v", err)
|
||||
}
|
||||
|
||||
return bin, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
func genConfig(t *testing.T, bin string) (config string, cleanup func()) {
|
||||
tmpl := template.Must(template.ParseFiles("test/hooks.json.tmpl"))
|
||||
|
||||
tmp, err := ioutil.TempDir("", "webhook-config-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanup == nil {
|
||||
os.RemoveAll(tmp)
|
||||
}
|
||||
}()
|
||||
|
||||
path := filepath.Join(tmp, "hooks.json")
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Creating config template: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data := struct{ Hookecho string }{filepath.ToSlash(bin)}
|
||||
if err := tmpl.Execute(file, data); err != nil {
|
||||
t.Fatalf("Executing template: %v", err)
|
||||
}
|
||||
|
||||
return path, func() { os.RemoveAll(tmp) }
|
||||
}
|
||||
|
||||
func buildWebhook(t *testing.T) (bin string, cleanup func()) {
|
||||
tmp, err := ioutil.TempDir("", "webhook-test-")
|
||||
if err != nil {
|
||||
|
@ -142,6 +200,18 @@ func killAndWait(cmd *exec.Cmd) {
|
|||
cmd.Wait()
|
||||
}
|
||||
|
||||
// webhookEnv returns the process environment without any existing hook
|
||||
// namespace variables.
|
||||
func webhookEnv() (env []string) {
|
||||
for _, v := range os.Environ() {
|
||||
if strings.HasPrefix(v, hook.EnvNamespace) {
|
||||
continue
|
||||
}
|
||||
env = append(env, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var hookHandlerTests = []struct {
|
||||
desc string
|
||||
id string
|
||||
|
@ -302,7 +372,7 @@ var hookHandlerTests = []struct {
|
|||
}`,
|
||||
false,
|
||||
http.StatusOK,
|
||||
`{"message":"","output":"1481a2de7b2a7d02428ad93446ab166be7793fbb Garen Torikian lolwut@noway.biz\n","error":""}`,
|
||||
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb Garen Torikian lolwut@noway.biz\nenv: HOOK_pusher.email=lolwut@noway.biz\n"}`,
|
||||
},
|
||||
{
|
||||
"bitbucket", // bitbucket sends their payload using uriencoded params.
|
||||
|
@ -311,7 +381,7 @@ var hookHandlerTests = []struct {
|
|||
`payload={"canon_url": "https://bitbucket.org","commits": [{"author": "marcus","branch": "master","files": [{"file": "somefile.py","type": "modified"}],"message": "Added some more things to somefile.py\n","node": "620ade18607a","parents": ["702c70160afc"],"raw_author": "Marcus Bertrand <marcus@somedomain.com>","raw_node": "620ade18607ac42d872b568bb92acaa9a28620e9","revision": null,"size": -1,"timestamp": "2012-05-30 05:58:56","utctimestamp": "2014-11-07 15:19:02+00:00"}],"repository": {"absolute_url": "/webhook/testing/","fork": false,"is_private": true,"name": "Project X","owner": "marcus","scm": "git","slug": "project-x","website": "https://atlassian.com/"},"user": "marcus"}`,
|
||||
true,
|
||||
http.StatusOK,
|
||||
`{"message":"success","output":"\n","error":""}`,
|
||||
`{"message":"success"}`,
|
||||
},
|
||||
{
|
||||
"gitlab",
|
||||
|
@ -361,7 +431,7 @@ var hookHandlerTests = []struct {
|
|||
}`,
|
||||
false,
|
||||
http.StatusOK,
|
||||
`{"message":"success","output":"b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com\n","error":""}`,
|
||||
`{"message":"success","output":"arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com\n"}`,
|
||||
},
|
||||
|
||||
{"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},
|
||||
|
|
Loading…
Add table
Reference in a new issue