Compare commits

..

14 Commits

Author SHA1 Message Date
Adnan Hajdarević b66216675a Merge pull request #107 from adnanh/development
2.6.0
2016-11-01 20:11:07 +01:00
Adnan Hajdarevic ecbcf11153 fix #106 2016-11-01 20:09:41 +01:00
Adnan Hajdarević 7d525cf317 Merge pull request #105 from adnanh/development
Fixes #103
2016-10-28 16:07:07 +02:00
Adnan Hajdarević e83d7029ff Merge pull request #104 from adnanh/master
Backmerge
2016-10-28 16:06:33 +02:00
Adnan Hajdarević 36c2c692d6 Add instructions for debian 2016-10-28 16:05:39 +02:00
Adnan Hajdarević 86cef3e421 Merge pull request #101 from adnanh/development
webhook 2.5.0
2016-09-29 20:11:46 +02:00
Adnan Hajdarevic 75cf8952be remove \n 2016-09-29 20:11:20 +02:00
Adnan Hajdarevic c53596df59 override content type header when returning error message 2016-09-29 20:08:47 +02:00
Adnan Hajdarevic cc0d9b2cba fix tests, return raw output, return 500 if the command did not execute properly - fixes #87
return raw stdout instead of json wrapped message - fixes #88
2016-09-29 19:57:06 +02:00
Adnan Hajdarević c6530b17e7 Merge pull request #100 from adnanh/env-names
Do not prefix EnvName with HOOK_
2016-09-29 19:20:06 +02:00
Adnan Hajdarevic 1943c5311f bump version to 2.5.0 2016-09-29 19:16:47 +02:00
Adnan Hajdarevic 3b59539a33 do not prefix specified environment variable name with HOOK_ (fixes #98) 2016-09-29 19:15:51 +02:00
Adnan Hajdarević 923b0c6daa Merge pull request #95 from denji/Makefile
Makefile cross-platform binary
2016-09-13 09:20:11 +02:00
Denis Denisov 8530255ae6 Makefile build cross-binary 2016-09-12 23:09:05 +03:00
7 changed files with 135 additions and 69 deletions
+29
View File
@@ -0,0 +1,29 @@
OS = darwin freebsd linux openbsd windows
ARCHS = 386 arm amd64 arm64
all: build release
build: deps
go build
release: clean deps
@for arch in $(ARCHS);\
do \
for os in $(OS);\
do \
echo "Building $$os-$$arch"; \
mkdir -p build/webhook-$$os-$$arch/; \
GOOS=$$os GOARCH=$$arch go build -o build/webhook-$$os-$$arch/webhook; \
tar cz -C build -f build/webhook-$$os-$$arch.tar.gz webhook-$$os-$$arch; \
done \
done
test: deps
go test ./...
deps:
go get -d -v -t ./...
clean:
rm -rf build
rm -f webhook
+7
View File
@@ -19,12 +19,19 @@ Everything else is the responsibility of the command's author.
--- ---
# Getting started # Getting started
## Installation
### Building from source
To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the To get started, first make sure you've properly set up your [Golang](http://golang.org/doc/install) environment and then run the
```bash ```bash
$ go get github.com/adnanh/webhook $ go get github.com/adnanh/webhook
``` ```
to get the latest version of the [webhook](https://github.com/adnanh/webhook/). to get the latest version of the [webhook](https://github.com/adnanh/webhook/).
### Using package manager
#### Debian "sid"
If you are using unstable version of Debian linux ("sid"), you can install webhook using `apt-get install webhook` which will install community packaged version (thanks [@freeekanayaka](https://github.com/freeekanayaka)) from https://packages.debian.org/sid/webhook
## Configuration
Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them. Next step is to define some hooks you want [webhook](https://github.com/adnanh/webhook/) to serve. Begin by creating an empty file named `hooks.json`. This file will contain an array of hooks the [webhook](https://github.com/adnanh/webhook/) will serve. Check [Hook definition page](https://github.com/adnanh/webhook/wiki/Hook-Definition) to see the detailed description of what properties a hook can contain, and how to use them.
Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`. Let's define a simple hook named `redeploy-webhook` that will run a redeploy script located in `/var/scripts/redeploy.sh`.
+40 -21
View File
@@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/textproto"
"reflect" "reflect"
"regexp" "regexp"
"strconv" "strconv"
@@ -18,6 +19,7 @@ import (
const ( const (
SourceHeader string = "header" SourceHeader string = "header"
SourceQuery string = "url" SourceQuery string = "url"
SourceQueryAlias string = "query"
SourcePayload string = "payload" SourcePayload string = "payload"
SourceString string = "string" SourceString string = "string"
SourceEntirePayload string = "entire-payload" SourceEntirePayload string = "entire-payload"
@@ -205,11 +207,13 @@ type Argument struct {
// based on the Argument's source // based on the Argument's source
func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) { func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string, bool) {
var source *map[string]interface{} var source *map[string]interface{}
key := ha.Name
switch ha.Source { switch ha.Source {
case SourceHeader: case SourceHeader:
source = headers source = headers
case SourceQuery: key = textproto.CanonicalMIMEHeaderKey(ha.Name)
case SourceQuery, SourceQueryAlias:
source = query source = query
case SourcePayload: case SourcePayload:
source = payload source = payload
@@ -242,7 +246,7 @@ func (ha *Argument) Get(headers, query, payload *map[string]interface{}) (string
} }
if source != nil { if source != nil {
return ExtractParameterAsString(ha.Name, *source) return ExtractParameterAsString(key, *source)
} }
return "", false return "", false
@@ -300,7 +304,9 @@ type Hook struct {
// ParseJSONParameters decodes specified arguments to JSON objects and replaces the // ParseJSONParameters decodes specified arguments to JSON objects and replaces the
// string with the newly created object // string with the newly created object
func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) error { func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface{}) []error {
var errors = make([]error, 0)
for i := range h.JSONStringParameters { for i := range h.JSONStringParameters {
if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok { if arg, ok := h.JSONStringParameters[i].Get(headers, query, payload); ok {
var newArg map[string]interface{} var newArg map[string]interface{}
@@ -311,7 +317,8 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
err := decoder.Decode(&newArg) err := decoder.Decode(&newArg)
if err != nil { if err != nil {
return &ParseError{err} errors = append(errors, &ParseError{err})
continue
} }
var source *map[string]interface{} var source *map[string]interface{}
@@ -321,27 +328,38 @@ func (h *Hook) ParseJSONParameters(headers, query, payload *map[string]interface
source = headers source = headers
case SourcePayload: case SourcePayload:
source = payload source = payload
case SourceQuery: case SourceQuery, SourceQueryAlias:
source = query source = query
} }
if source != nil { if source != nil {
ReplaceParameter(h.JSONStringParameters[i].Name, source, newArg) key := h.JSONStringParameters[i].Name
if h.JSONStringParameters[i].Source == SourceHeader {
key = textproto.CanonicalMIMEHeaderKey(h.JSONStringParameters[i].Name)
}
ReplaceParameter(key, source, newArg)
} else { } else {
return &SourceError{h.JSONStringParameters[i]} errors = append(errors, &SourceError{h.JSONStringParameters[i]})
} }
} else { } else {
return &ArgumentError{h.JSONStringParameters[i]} errors = append(errors, &ArgumentError{h.JSONStringParameters[i]})
} }
} }
if len(errors) > 0 {
return errors
}
return nil return nil
} }
// ExtractCommandArguments creates a list of arguments, based on the // ExtractCommandArguments creates a list of arguments, based on the
// PassArgumentsToCommand property that is ready to be used with exec.Command() // PassArgumentsToCommand property that is ready to be used with exec.Command()
func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, error) { func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]interface{}) ([]string, []error) {
var args = make([]string, 0) var args = make([]string, 0)
var errors = make([]error, 0)
args = append(args, h.ExecuteCommand) args = append(args, h.ExecuteCommand)
@@ -350,33 +368,41 @@ func (h *Hook) ExtractCommandArguments(headers, query, payload *map[string]inter
args = append(args, arg) args = append(args, arg)
} else { } else {
args = append(args, "") args = append(args, "")
return args, &ArgumentError{h.PassArgumentsToCommand[i]} errors = append(errors, &ArgumentError{h.PassArgumentsToCommand[i]})
} }
} }
if len(errors) > 0 {
return args, errors
}
return args, nil return args, nil
} }
// ExtractCommandArgumentsForEnv creates a list of arguments in key=value // ExtractCommandArgumentsForEnv creates a list of arguments in key=value
// format, based on the PassEnvironmentToCommand property that is ready to be used // format, based on the PassEnvironmentToCommand property that is ready to be used
// with exec.Command(). // with exec.Command().
func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, error) { func (h *Hook) ExtractCommandArgumentsForEnv(headers, query, payload *map[string]interface{}) ([]string, []error) {
var args = make([]string, 0) var args = make([]string, 0)
var errors = make([]error, 0)
for i := range h.PassEnvironmentToCommand { for i := range h.PassEnvironmentToCommand {
if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok { if arg, ok := h.PassEnvironmentToCommand[i].Get(headers, query, payload); ok {
if h.PassEnvironmentToCommand[i].EnvName != "" { if h.PassEnvironmentToCommand[i].EnvName != "" {
// first try to use the EnvName if specified // first try to use the EnvName if specified
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].EnvName+"="+arg) args = append(args, h.PassEnvironmentToCommand[i].EnvName+"="+arg)
} else { } else {
// then fallback on the name // then fallback on the name
args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg) args = append(args, EnvNamespace+h.PassEnvironmentToCommand[i].Name+"="+arg)
} }
} else { } else {
return args, &ArgumentError{h.PassEnvironmentToCommand[i]} errors = append(errors, &ArgumentError{h.PassEnvironmentToCommand[i]})
} }
} }
if len(errors) > 0 {
return args, errors
}
return args, nil return args, nil
} }
@@ -521,10 +547,3 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod
} }
return false, nil return false, nil
} }
// CommandStatusResponse type encapsulates the executed command exit code, message, stdout and stderr
type CommandStatusResponse struct {
ResponseMessage string `json:"message,omitempty"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
}
+1 -1
View File
@@ -179,7 +179,7 @@ var hookExtractCommandArgumentsForEnvTests = []struct {
"test", "test",
[]Argument{Argument{"header", "a", "MYKEY"}}, []Argument{Argument{"header", "a", "MYKEY"}},
&map[string]interface{}{"a": "z"}, nil, nil, &map[string]interface{}{"a": "z"}, nil, nil,
[]string{"HOOK_MYKEY=z"}, []string{"MYKEY=z"},
true, true,
}, },
// failures // failures
+1 -2
View File
@@ -57,7 +57,7 @@
"id": "bitbucket", "id": "bitbucket",
"execute-command": "{{ .Hookecho }}", "execute-command": "{{ .Hookecho }}",
"command-working-directory": "/", "command-working-directory": "/",
"include-command-output-in-response": true, "include-command-output-in-response": false,
"response-message": "success", "response-message": "success",
"parse-parameters-as-json": [ "parse-parameters-as-json": [
{ {
@@ -136,4 +136,3 @@
} }
} }
] ]
+46 -40
View File
@@ -21,20 +21,21 @@ import (
) )
const ( const (
version = "2.4.0" version = "2.6.0"
) )
var ( var (
ip = flag.String("ip", "0.0.0.0", "ip the webhook should serve hooks on") ip = flag.String("ip", "0.0.0.0", "ip the webhook should serve hooks on")
port = flag.Int("port", 9000, "port the webhook should serve hooks on") port = flag.Int("port", 9000, "port the webhook should serve hooks on")
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") 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")
key = flag.String("key", "key.pem", "path to the HTTPS certificate private key 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 responseHeaders hook.ResponseHeaders
@@ -51,6 +52,11 @@ func main() {
flag.Parse() flag.Parse()
if *justDisplayVersion {
fmt.Println("webhook version " + version)
os.Exit(0)
}
log.SetPrefix("[webhook] ") log.SetPrefix("[webhook] ")
log.SetFlags(log.Ldate | log.Ltime) log.SetFlags(log.Ldate | log.Ltime)
@@ -191,12 +197,10 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
} }
// handle hook // handle hook
if err = matchedHook.ParseJSONParameters(&headers, &query, &payload); err != nil { if errors := matchedHook.ParseJSONParameters(&headers, &query, &payload); errors != nil {
msg := fmt.Sprintf("error parsing JSON parameters: %s", err) for _, err := range errors {
log.Printf(msg) log.Printf("error parsing JSON parameters: %s\n", err)
w.WriteHeader(http.StatusBadRequest) }
fmt.Fprintf(w, "Unable to parse JSON parameters.")
return
} }
var ok bool var ok bool
@@ -222,8 +226,15 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
} }
if matchedHook.CaptureCommandOutput { if matchedHook.CaptureCommandOutput {
response := handleHook(matchedHook, &headers, &query, &payload, &body) response, err := handleHook(matchedHook, &headers, &query, &payload, &body)
fmt.Fprintf(w, response)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error occurred while executing the hook's command. Please check your logs for more details.")
} else {
fmt.Fprintf(w, response)
}
} else { } else {
go handleHook(matchedHook, &headers, &query, &payload, &body) go handleHook(matchedHook, &headers, &query, &payload, &body)
fmt.Fprintf(w, matchedHook.ResponseMessage) fmt.Fprintf(w, matchedHook.ResponseMessage)
@@ -241,48 +252,43 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) string { func handleHook(h *hook.Hook, headers, query, payload *map[string]interface{}, body *[]byte) (string, error) {
var err error var errors []error
cmd := exec.Command(h.ExecuteCommand) cmd := exec.Command(h.ExecuteCommand)
cmd.Dir = h.CommandWorkingDirectory cmd.Dir = h.CommandWorkingDirectory
cmd.Args, err = h.ExtractCommandArguments(headers, query, payload) cmd.Args, errors = h.ExtractCommandArguments(headers, query, payload)
if err != nil { if errors != nil {
log.Printf("error extracting command arguments: %s", err) for _, err := range errors {
log.Printf("error extracting command arguments: %s\n", err)
}
} }
var envs []string var envs []string
envs, err = h.ExtractCommandArgumentsForEnv(headers, query, payload) envs, errors = h.ExtractCommandArgumentsForEnv(headers, query, payload)
if err != nil {
log.Printf("error extracting command arguments for environment: %s", err) if errors != nil {
for _, err := range errors {
log.Printf("error extracting command arguments for environment: %s\n", err)
}
} }
cmd.Env = append(os.Environ(), envs...) cmd.Env = append(os.Environ(), envs...)
log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir) log.Printf("executing %s (%s) with arguments %q and environment %s using %s as cwd\n", h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir)
out, err := cmd.CombinedOutput() out, err := cmd.Output()
log.Printf("command output: %s\n", out) log.Printf("command output: %s\n", out)
var errorResponse string
if err != nil { if err != nil {
log.Printf("error occurred: %+v\n", err) log.Printf("error occurred: %+v\n", err)
errorResponse = fmt.Sprintf("%+v", err)
} }
log.Printf("finished handling %s\n", h.ID) log.Printf("finished handling %s\n", h.ID)
var response []byte return string(out), err
response, err = json.Marshal(&hook.CommandStatusResponse{ResponseMessage: h.ResponseMessage, Output: string(out), Error: errorResponse})
if err != nil {
log.Printf("error marshalling response: %+v", err)
return h.ResponseMessage
}
return string(response)
} }
func reloadHooks() { func reloadHooks() {
+11 -5
View File
@@ -372,7 +372,9 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`, `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`,
}, },
{ {
"bitbucket", // bitbucket sends their payload using uriencoded params. "bitbucket", // bitbucket sends their payload using uriencoded params.
@@ -381,7 +383,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"}`, `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, true,
http.StatusOK, http.StatusOK,
`{"message":"success"}`, `success`,
}, },
{ {
"gitlab", "gitlab",
@@ -431,7 +433,8 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"message":"success","output":"arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com\n"}`, `arg: b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327 John Smith john@example.com
`,
}, },
{ {
@@ -469,7 +472,9 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\nenv: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00\n"}`, `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
env: HOOK_head_commit.timestamp=2013-03-12T08:14:29-07:00
`,
}, },
{ {
@@ -506,7 +511,8 @@ var hookHandlerTests = []struct {
}`, }`,
false, false,
http.StatusOK, http.StatusOK,
`{"output":"arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz\n"}`, `arg: 1481a2de7b2a7d02428ad93446ab166be7793fbb lolwut@noway.biz
`,
}, },
{"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`}, {"empty payload", "github", nil, `{}`, false, http.StatusOK, `Hook rules were not satisfied.`},