mirror of
https://github.com/adnanh/webhook.git
synced 2026-07-27 01:29:16 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 857e708f87 | |||
| f78eeeca7d | |||
| 8a2d3f85cd | |||
| 34d4418840 | |||
| 7ea2a53bbb | |||
| 2da9957e86 | |||
| 5981da2717 | |||
| 1b13355196 | |||
| eb7e8f5ba8 |
@@ -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/)
|
||||
|
||||
+127
-6
@@ -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
|
||||
[
|
||||
@@ -673,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -20,6 +20,11 @@ There are four types of request values:
|
||||
```
|
||||
|
||||
3. HTTP Request parameters
|
||||
#### Valid `name` Parameters:
|
||||
- `"name": "method"`
|
||||
- `"name": "remote-addr"`
|
||||
|
||||
*Note* Anything other than above mentioned `name` parameters would be invalid!
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -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/
|
||||
|
||||
+32
-4
@@ -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 {
|
||||
@@ -824,7 +828,7 @@ func (r Rules) Evaluate(req *Request) (bool, error) {
|
||||
return r.Match.Evaluate(req)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AndRule will evaluate to true if and only if all of the ChildRules evaluate to true
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
+15
-16
@@ -351,21 +351,20 @@ func TestHookExtractCommandArguments(t *testing.T) {
|
||||
// we test both cases where the name of the data is used as the name of the
|
||||
// env key & the case where the hook definition sets the env var name to a
|
||||
// fixed value using the envname construct like so::
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// [
|
||||
// {
|
||||
// "id": "push",
|
||||
// "execute-command": "bb2mm",
|
||||
// "command-working-directory": "/tmp",
|
||||
// "pass-environment-to-command":
|
||||
// [
|
||||
// {
|
||||
// "source": "entire-payload",
|
||||
// "envname": "PAYLOAD"
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||
exec string
|
||||
args []Argument
|
||||
@@ -597,7 +596,7 @@ var andRuleTests = []struct {
|
||||
[]byte{},
|
||||
true, false,
|
||||
},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, true, false},
|
||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||
// failures
|
||||
{
|
||||
"invalid rule",
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "2.8.2"
|
||||
version = "2.8.3"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -172,7 +172,7 @@ func main() {
|
||||
}
|
||||
|
||||
if !*verbose {
|
||||
log.SetOutput(io.Discard)
|
||||
log.SetOutput(ioutil.Discard)
|
||||
}
|
||||
|
||||
// Create pidfile
|
||||
@@ -379,7 +379,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
isMultipart := strings.HasPrefix(req.ContentType, "multipart/form-data;")
|
||||
|
||||
if !isMultipart {
|
||||
req.Body, err = io.ReadAll(r.Body)
|
||||
req.Body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("[%s] error reading the request body: %+v\n", req.ID, err)
|
||||
}
|
||||
@@ -608,7 +608,7 @@ func handleHook(h *hook.Hook, r *hook.Request) (string, error) {
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
tmpfile, err := os.CreateTemp(h.CommandWorkingDirectory, files[i].EnvName)
|
||||
tmpfile, err := ioutil.TempFile(h.CommandWorkingDirectory, files[i].EnvName)
|
||||
if err != nil {
|
||||
log.Printf("[%s] error creating temp file [%s]", r.ID, err)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user