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)
|
- [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)
|
- [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
|
- [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)
|
- [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/)
|
- [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/)
|
- [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)
|
* [Multipart Form Data](#multipart-form-data)
|
||||||
* [Pass string arguments to command](#pass-string-arguments-to-command)
|
* [Pass string arguments to command](#pass-string-arguments-to-command)
|
||||||
* [Receive Synology DSM notifications](#receive-synology-notifications)
|
* [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
|
## 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
|
## Incoming Gitea webhook
|
||||||
|
|
||||||
|
JSON version:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -228,7 +251,7 @@ Values in the request body can be accessed in the command or to the match rule b
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "payload",
|
"source": "payload",
|
||||||
"name": "pusher.name"
|
"name": "pusher.full_name"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "payload",
|
"source": "payload",
|
||||||
@@ -242,12 +265,12 @@ Values in the request body can be accessed in the command or to the match rule b
|
|||||||
{
|
{
|
||||||
"match":
|
"match":
|
||||||
{
|
{
|
||||||
"type": "value",
|
"type": "payload-hmac-sha256",
|
||||||
"value": "mysecret",
|
"secret": "mysecret",
|
||||||
"parameter":
|
"parameter":
|
||||||
{
|
{
|
||||||
"source": "payload",
|
"source": "header",
|
||||||
"name": "secret"
|
"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":
|
"match":
|
||||||
{
|
{
|
||||||
"type": "value",
|
"type": "value",
|
||||||
"value": "refs/heads/master",
|
"value": "refs/heads/main",
|
||||||
"parameter":
|
"parameter":
|
||||||
{
|
{
|
||||||
"source": "payload",
|
"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
|
## Slack slash command
|
||||||
```json
|
```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
|
3. HTTP Request parameters
|
||||||
|
#### Valid `name` Parameters:
|
||||||
|
- `"name": "method"`
|
||||||
|
- `"name": "remote-addr"`
|
||||||
|
|
||||||
|
*Note* Anything other than above mentioned `name` parameters would be invalid!
|
||||||
|
|
||||||
```json
|
```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
|
[w]: https://github.com/adnanh/webhook
|
||||||
[tt]: https://golang.org/pkg/text/template/
|
[tt]: https://golang.org/pkg/text/template/
|
||||||
|
|||||||
+32
-4
@@ -13,12 +13,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -750,14 +750,18 @@ func (h *Hooks) LoadFromFile(path string, asTemplate bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse hook file for hooks
|
// parse hook file for hooks
|
||||||
file, e := ioutil.ReadFile(path)
|
file, e := os.ReadFile(path)
|
||||||
|
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
if asTemplate {
|
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))
|
tmpl, err := template.New("hooks").Funcs(funcMap).Parse(string(file))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -824,7 +828,7 @@ func (r Rules) Evaluate(req *Request) (bool, error) {
|
|||||||
return r.Match.Evaluate(req)
|
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
|
// 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 {
|
func getenv(s string) string {
|
||||||
return os.Getenv(s)
|
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
|
// 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
|
// env key & the case where the hook definition sets the env var name to a
|
||||||
// fixed value using the envname construct like so::
|
// fixed value using the envname construct like so::
|
||||||
//
|
// [
|
||||||
// [
|
// {
|
||||||
// {
|
// "id": "push",
|
||||||
// "id": "push",
|
// "execute-command": "bb2mm",
|
||||||
// "execute-command": "bb2mm",
|
// "command-working-directory": "/tmp",
|
||||||
// "command-working-directory": "/tmp",
|
// "pass-environment-to-command":
|
||||||
// "pass-environment-to-command":
|
// [
|
||||||
// [
|
// {
|
||||||
// {
|
// "source": "entire-payload",
|
||||||
// "source": "entire-payload",
|
// "envname": "PAYLOAD"
|
||||||
// "envname": "PAYLOAD"
|
// },
|
||||||
// },
|
// ]
|
||||||
// ]
|
// }
|
||||||
// }
|
// ]
|
||||||
// ]
|
|
||||||
var hookExtractCommandArgumentsForEnvTests = []struct {
|
var hookExtractCommandArgumentsForEnvTests = []struct {
|
||||||
exec string
|
exec string
|
||||||
args []Argument
|
args []Argument
|
||||||
@@ -597,7 +596,7 @@ var andRuleTests = []struct {
|
|||||||
[]byte{},
|
[]byte{},
|
||||||
true, false,
|
true, false,
|
||||||
},
|
},
|
||||||
{"empty rule", AndRule{{}}, nil, nil, nil, nil, true, false},
|
{"empty rule", AndRule{{}}, nil, nil, nil, nil, false, false},
|
||||||
// failures
|
// failures
|
||||||
{
|
{
|
||||||
"invalid rule",
|
"invalid rule",
|
||||||
|
|||||||
+5
-5
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "2.8.2"
|
version = "2.8.3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -172,7 +172,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !*verbose {
|
if !*verbose {
|
||||||
log.SetOutput(io.Discard)
|
log.SetOutput(ioutil.Discard)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create pidfile
|
// Create pidfile
|
||||||
@@ -379,7 +379,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
isMultipart := strings.HasPrefix(req.ContentType, "multipart/form-data;")
|
isMultipart := strings.HasPrefix(req.ContentType, "multipart/form-data;")
|
||||||
|
|
||||||
if !isMultipart {
|
if !isMultipart {
|
||||||
req.Body, err = io.ReadAll(r.Body)
|
req.Body, err = ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[%s] error reading the request body: %+v\n", req.ID, err)
|
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 {
|
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 {
|
if err != nil {
|
||||||
log.Printf("[%s] error creating temp file [%s]", r.ID, err)
|
log.Printf("[%s] error creating temp file [%s]", r.ID, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user