diff --git a/Makefile b/Makefile index 1a06482..903b7a3 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ release: clean deps ## Generate releases for unix systems do \ echo "Building $$os-$$arch"; \ mkdir -p build/webhook-$$os-$$arch/; \ - GOOS=$$os GOARCH=$$arch go build -o build/webhook-$$os-$$arch/webhook; \ + CGO_ENABLED=0 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 diff --git a/README.md b/README.md index c9b43a5..20dd6a8 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ If you use Mattermost or Slack, you can set up an "Outgoing webhook integration" Everything else is the responsibility of the command's author. ## Not what you're looking for? -| hookdoo | hookdeck | +| hookdoo | hookdeck | | :-: | :-: | -| Scriptable webhook gateway to safely run your custom builds, deploys, and proxy scripts on your servers. | Inspect, monitor and replay webhooks without the back and forth troubleshooting. | +| Scriptable webhook gateway to safely run your custom builds, deploys, and proxy scripts on your servers. | An event gateway to reliably ingest, verify, queue, transform, filter, inspect, monitor, and replay webhooks. | # Getting started @@ -141,7 +141,7 @@ Check out [Hook examples page](docs/Hook-Examples.md) for more complex examples - [Using Prometheus to Automatically Scale WebLogic Clusters on Kubernetes](https://blogs.oracle.com/weblogicserver/using-prometheus-to-automatically-scale-weblogic-clusters-on-kubernetes-v5) by [Marina Kogan](https://blogs.oracle.com/author/9a4fe754-1cc2-4c64-95fc-360642b62927) - [Github Pages and Jekyll - A New Platform for LACNIC Labs](https://labs.lacnic.net/a-new-platform-for-lacniclabs/) by [Carlos Martínez Cagnazzo](https://twitter.com/carlosm3011) - [How to Deploy React Apps Using Webhooks and Integrating Slack on Ubuntu](https://www.alibabacloud.com/blog/how-to-deploy-react-apps-using-webhooks-and-integrating-slack-on-ubuntu_594116) by Arslan Ud Din Shafiq - - [Private webhooks](https://ihateithe.re/2018/01/private-webhooks/) by [Thomas](https://ihateithe.re/colophon/) + - [Private webhooks](https://tmertz.com/2018/01/private-webhooks/) by [Thomas](https://tmertz.com) - [Adventures in webhooks](https://medium.com/@draketech/adventures-in-webhooks-2d6584501c62) by [Drake](https://medium.com/@draketech) - [GitHub pro tips](http://notes.spencerlyon.com/2016/01/04/github-pro-tips/) by [Spencer Lyon](http://notes.spencerlyon.com/) - [XiaoMi Vacuum + Amazon Button = Dash Cleaning](https://www.instructables.com/id/XiaoMi-Vacuum-Amazon-Button-Dash-Cleaning/) by [c0mmensal](https://www.instructables.com/member/c0mmensal/) diff --git a/docs/Hook-Examples.md b/docs/Hook-Examples.md index 7d07932..66aa5a7 100644 --- a/docs/Hook-Examples.md +++ b/docs/Hook-Examples.md @@ -22,6 +22,25 @@ although the examples on this page all use the JSON format. * [Multipart Form Data](#multipart-form-data) * [Pass string arguments to command](#pass-string-arguments-to-command) * [Receive Synology DSM notifications](#receive-synology-notifications) +* [Incoming Azure Container Registry (ACR) webhook](#incoming-acr-webhook) + +## Printing the Raw Webhook Payload to Standard Output + +This hook configuration receives incoming webhook requests and prints the raw request body (payload) directly to the server's standard output (visible in the webhook process logs when running with -verbose). It is particularly useful for debugging and verifying webhook deliveries from external services. + +```json +[ + { + "id": "print-payload", + "execute-command": "/bin/echo", + "pass-arguments-to-command": [ + { + "source": "entire-payload", + } + ] + } +] +``` ## Incoming Github webhook @@ -213,7 +232,11 @@ Values in the request body can be accessed in the command or to the match rule b } ] ``` + ## Incoming Gitea webhook + +JSON version: + ```json [ { @@ -228,7 +251,7 @@ Values in the request body can be accessed in the command or to the match rule b }, { "source": "payload", - "name": "pusher.name" + "name": "pusher.full_name" }, { "source": "payload", @@ -242,12 +265,12 @@ Values in the request body can be accessed in the command or to the match rule b { "match": { - "type": "value", - "value": "mysecret", + "type": "payload-hmac-sha256", + "secret": "mysecret", "parameter": { - "source": "payload", - "name": "secret" + "source": "header", + "name": "X-Gitea-Signature" } } }, @@ -255,7 +278,7 @@ Values in the request body can be accessed in the command or to the match rule b "match": { "type": "value", - "value": "refs/heads/master", + "value": "refs/heads/main", "parameter": { "source": "payload", @@ -269,6 +292,35 @@ Values in the request body can be accessed in the command or to the match rule b ] ``` +YAML version: + +```yaml +- id: webhook + execute-command: /home/adnan/redeploy-go-webhook.sh + command-working-directory: /home/adnan/go + pass-arguments-to-command: + - source: payload + name: head_commit.id + - source: payload + name: pusher.full_name + - source: payload + name: pusher.email + trigger-rule: + and: + - match: + type: payload-hmac-sha256 + secret: mysecret + parameter: + source: header + name: X-Gitea-Signature + - match: + type: value + value: refs/heads/main + parameter: + source: payload + name: ref +``` + ## Slack slash command ```json [ @@ -498,7 +550,8 @@ A reference to the second item in the array would look like this: [ { "id": "sendgrid", - "execute-command": "{{ .Hookecho }}", + "execute-command": "/root/my-server/deployment.sh", + "command-working-directory": "/root/my-server", "trigger-rule": { "match": { "type": "value", @@ -672,3 +725,72 @@ Webhooks feature introduced in DSM 7.x seems to be incomplete & broken, but you } ] ``` +## Incoming Azure Container Registry (ACR) webhook + +ACR can send webhooks on image push events. The `hooks.json` below will handle those events and pass relevant properties as environment variables to a command. + +Here is an example of a working docker webhook container used to handle the webhooks and fill the cache of a local registry: [ACR Harbor local cache feeder](https://github.com/tomdess/registry-cache-feeder). + + +```json +[ + { + "id": "acr-push-event", + "execute-command": "/config/script-acr.sh", + "command-working-directory": "/config", + "pass-environment-to-command": + [ + { + "envname": "ACTION", + "source": "payload", + "name": "action" + }, + { + "envname": "REPO", + "source": "payload", + "name": "target.repository" + }, + { + "envname": "TAG", + "source": "payload", + "name": "target.tag" + }, + { + "envname": "DIGEST", + "source": "payload", + "name": "target.digest" + } + ], + "trigger-rule": + { + "and": + [ + { + "match": + { + "type": "value", + "value": "mysecretToken", + "parameter": + { + "source": "header", + "name": "X-Static-Token" + } + } + }, + { + "match": + { + "type": "value", + "value": "push", + "parameter": + { + "source": "payload", + "name": "action" + } + } + } + ] + } + } +] +``` diff --git a/docs/Templates.md b/docs/Templates.md index 35f10a0..3355112 100644 --- a/docs/Templates.md +++ b/docs/Templates.md @@ -73,5 +73,38 @@ Additionally, the result is piped through the built-in Go template function `js` ``` +## Template Functions + +In addition to the [built-in Go template functions and features][tt], `webhook` provides the following functions: + +### `getenv` + +The `getenv` template function can be used for inserting environment variables into a templated configuration file. + +Example: +``` +"Secret": "{{getenv TEST_secret | js}}" +``` + +### `cat` + +The `cat` template function can be used to read a file from the local filesystem. This is useful for reading secrets from files. If the file doesn't exist, it returns an empty string. + +Example: +``` +"secret": "{{ cat "/run/secrets/my-secret" | js }}" +``` + +### `credential` + +The `credential` template function provides a way to retrieve secrets using [systemd's LoadCredential mechanism](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Credentials). It reads the file specified by the given name from the directory specified in the `CREDENTIALS_DIRECTORY` environment variable. + +If `CREDENTIALS_DIRECTORY` is not set, it will fall back to using `getenv` to read the secret from an environment variable of the given name. + +Example: +``` +"secret": "{{ credential "my-secret" | js }}" +``` + [w]: https://github.com/adnanh/webhook [tt]: https://golang.org/pkg/text/template/ diff --git a/images/hookdeck-black.svg b/images/hookdeck-black.svg new file mode 100644 index 0000000..962b07d --- /dev/null +++ b/images/hookdeck-black.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/images/hookdeck-white.svg b/images/hookdeck-white.svg new file mode 100644 index 0000000..19afbbf --- /dev/null +++ b/images/hookdeck-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 6699eeb..394dd79 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -13,12 +13,12 @@ import ( "errors" "fmt" "hash" - "io/ioutil" "log" "math" "net" "net/textproto" "os" + "path" "reflect" "regexp" "strconv" @@ -750,14 +750,18 @@ func (h *Hooks) LoadFromFile(path string, asTemplate bool) error { } // parse hook file for hooks - file, e := ioutil.ReadFile(path) + file, e := os.ReadFile(path) if e != nil { return e } if asTemplate { - funcMap := template.FuncMap{"getenv": getenv} + funcMap := template.FuncMap{ + "cat": cat, + "credential": credential, + "getenv": getenv, + } tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file)) if err != nil { @@ -956,3 +960,27 @@ func compare(a, b string) bool { func getenv(s string) string { return os.Getenv(s) } + +// cat provides a template function to retrieve content of files +// Similarly to getenv, if no file is found, it returns the empty string +func cat(s string) string { + data, e := os.ReadFile(s) + + if e != nil { + return "" + } + + return strings.TrimSuffix(string(data), "\n") +} + +// credential provides a template function to retreive secrets using systemd's LoadCredential mechanism +func credential(s string) string { + dir := getenv("CREDENTIALS_DIRECTORY") + + // If no credential directory is found, fallback to the env variable + if dir == "" { + return getenv(s) + } + + return cat(path.Join(dir, s)) +}