Skip to content

Commit cc71f34

Browse files
committed
Introduce Webhook Channel
In its initial form, the webhook channel can be used to send notifications against a HTTP/HTTPS web server with a dynamic configurable URL and request body message.
1 parent 8c12d37 commit cc71f34

File tree

1 file changed

+170
-0
lines changed

1 file changed

+170
-0
lines changed

cmd/channel/webhook/main.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/icinga/icinga-notifications/internal"
8+
"github.com/icinga/icinga-notifications/pkg/plugin"
9+
"io"
10+
"net/http"
11+
"slices"
12+
"strconv"
13+
"strings"
14+
"text/template"
15+
)
16+
17+
type Webhook struct {
18+
Method string `json:"method"`
19+
URLTemplate string `json:"url_template"`
20+
RequestBodyTemplate string `json:"request_body_template"`
21+
ResponseStatusCodes string `json:"response_status_codes"`
22+
23+
tmplUrl *template.Template
24+
tmplRequestBody *template.Template
25+
26+
respStatusCodes []int
27+
}
28+
29+
func (ch *Webhook) GetInfo() *plugin.Info {
30+
elements := []*plugin.ConfigOption{
31+
{
32+
Name: "method",
33+
Type: "string",
34+
Label: map[string]string{
35+
"en_US": "HTTP Method",
36+
"de_DE": "HTTP-Methode",
37+
},
38+
Help: map[string]string{
39+
"en_US": "HTTP request method used for the web request.",
40+
"de_DE": "HTTP-Methode für die Anfrage.",
41+
},
42+
Default: "POST",
43+
Required: true,
44+
},
45+
{
46+
Name: "url_template",
47+
Type: "string",
48+
Label: map[string]string{
49+
"en_US": "URL Template",
50+
"de_DE": "URL-Template",
51+
},
52+
Help: map[string]string{
53+
"en_US": "URL, optionally as a Go template over the current plugin.NotificationRequest.",
54+
"de_DE": "URL, optional als Go-Template über das zu verarbeitende plugin.NotificationRequest.",
55+
},
56+
Required: true,
57+
},
58+
{
59+
Name: "request_body_template",
60+
Type: "string",
61+
Label: map[string]string{
62+
"en_US": "Request Body Template",
63+
"de_DE": "Anfragedaten-Template",
64+
},
65+
Help: map[string]string{
66+
"en_US": "Go template applied to the current plugin.NotificationRequest to create an request body.",
67+
"de_DE": "Go-Template über das zu verarbeitende plugin.NotificationRequest zum Erzeugen der mitgesendeten Anfragedaten.",
68+
},
69+
Default: `{{json .}}`,
70+
},
71+
{
72+
Name: "response_status_codes",
73+
Type: "string",
74+
Label: map[string]string{
75+
"en_US": "Response Status Codes",
76+
"de_DE": "Antwort-Status-Codes",
77+
},
78+
Help: map[string]string{
79+
"en_US": "Comma separated list of expected HTTP response status code, e.g., 200,201,202,208,418",
80+
"de_DE": "Kommaseparierte Liste erwarteter Status-Code der HTTP-Antwort, z.B.: 200,201,202,208,418",
81+
},
82+
Default: "200",
83+
Required: true,
84+
},
85+
}
86+
87+
configAttrs, err := json.Marshal(elements)
88+
if err != nil {
89+
panic(err)
90+
}
91+
92+
return &plugin.Info{
93+
Name: "Webhook",
94+
Version: internal.Version.Version,
95+
Author: "Icinga GmbH",
96+
ConfigAttributes: configAttrs,
97+
}
98+
}
99+
100+
func (ch *Webhook) SetConfig(jsonStr json.RawMessage) error {
101+
err := json.Unmarshal(jsonStr, ch)
102+
if err != nil {
103+
return err
104+
}
105+
106+
tmplFuncs := template.FuncMap{
107+
"json": func(a any) (string, error) {
108+
data, err := json.Marshal(a)
109+
if err != nil {
110+
return "", err
111+
}
112+
return string(data), nil
113+
114+
},
115+
}
116+
117+
ch.tmplUrl, err = template.New("url").Funcs(tmplFuncs).Parse(ch.URLTemplate)
118+
if err != nil {
119+
return fmt.Errorf("cannot parse URL template: %w", err)
120+
}
121+
122+
ch.tmplRequestBody, err = template.New("request_body").Funcs(tmplFuncs).Parse(ch.RequestBodyTemplate)
123+
if err != nil {
124+
return fmt.Errorf("cannot parse Request Body template: %w", err)
125+
}
126+
127+
respStatusCodes := strings.Split(ch.ResponseStatusCodes, ",")
128+
ch.respStatusCodes = make([]int, len(respStatusCodes))
129+
for i, respStatusCodeStr := range respStatusCodes {
130+
respStatusCode, err := strconv.Atoi(respStatusCodeStr)
131+
if err != nil {
132+
return fmt.Errorf("cannot convert status code %q to int: %w", respStatusCodeStr, err)
133+
}
134+
ch.respStatusCodes[i] = respStatusCode
135+
}
136+
137+
return nil
138+
}
139+
140+
func (ch *Webhook) SendNotification(req *plugin.NotificationRequest) error {
141+
var urlBuff, reqBodyBuff bytes.Buffer
142+
if err := ch.tmplUrl.Execute(&urlBuff, req); err != nil {
143+
return fmt.Errorf("cannot execute URL template: %w", err)
144+
}
145+
if err := ch.tmplRequestBody.Execute(&reqBodyBuff, req); err != nil {
146+
return fmt.Errorf("cannot execute Request Body template: %w", err)
147+
}
148+
149+
httpReq, err := http.NewRequest(ch.Method, urlBuff.String(), &reqBodyBuff)
150+
if err != nil {
151+
return err
152+
}
153+
httpResp, err := http.DefaultClient.Do(httpReq)
154+
if err != nil {
155+
return err
156+
}
157+
_, _ = io.Copy(io.Discard, httpResp.Body)
158+
_ = httpResp.Body.Close()
159+
160+
if !slices.Contains(ch.respStatusCodes, httpResp.StatusCode) {
161+
return fmt.Errorf("unaccepted HTTP response status code %d not in %v",
162+
httpResp.StatusCode, ch.respStatusCodes)
163+
}
164+
165+
return nil
166+
}
167+
168+
func main() {
169+
plugin.RunPlugin(&Webhook{})
170+
}

0 commit comments

Comments
 (0)