From 8c12d3718a84fde2f36f3b868093a202dac01002 Mon Sep 17 00:00:00 2001 From: Alvar Penning Date: Fri, 12 Jan 2024 10:35:13 +0100 Subject: [PATCH 1/2] plugin: Document ConfigOption's Help field --- pkg/plugin/plugin.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index ed1dcaa8..36698eb1 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -36,7 +36,13 @@ type ConfigOption struct { // An "en_US" locale must be given as a fallback Label map[string]string `json:"label"` - // Element title: When the user moves the mouse pointer over an element, a tooltip is displayed with a given message. + // Element description map. Locale in the standard format (language_REGION) as key and corresponding label as value. + // Locale is assumed to be UTF-8 encoded (Without the suffix in the locale) + // + // When the user moves the mouse pointer over an element in the web UI, a tooltip is displayed with a given message. + // + // e.g. {"en_US": "HTTP request method for the request.", "de_DE": "HTTP-Methode für die Anfrage."} + // An "en_US" locale must be given as a fallback Help map[string]string `json:"help,omitempty"` // Element default: bool for checkbox default value, string for other elements (used as placeholder) From cc71f34eef90b98974e03475871a13f18033f255 Mon Sep 17 00:00:00 2001 From: Alvar Penning Date: Fri, 12 Jan 2024 10:35:35 +0100 Subject: [PATCH 2/2] 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. --- cmd/channel/webhook/main.go | 170 ++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 cmd/channel/webhook/main.go diff --git a/cmd/channel/webhook/main.go b/cmd/channel/webhook/main.go new file mode 100644 index 00000000..17d100c2 --- /dev/null +++ b/cmd/channel/webhook/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/icinga/icinga-notifications/internal" + "github.com/icinga/icinga-notifications/pkg/plugin" + "io" + "net/http" + "slices" + "strconv" + "strings" + "text/template" +) + +type Webhook struct { + Method string `json:"method"` + URLTemplate string `json:"url_template"` + RequestBodyTemplate string `json:"request_body_template"` + ResponseStatusCodes string `json:"response_status_codes"` + + tmplUrl *template.Template + tmplRequestBody *template.Template + + respStatusCodes []int +} + +func (ch *Webhook) GetInfo() *plugin.Info { + elements := []*plugin.ConfigOption{ + { + Name: "method", + Type: "string", + Label: map[string]string{ + "en_US": "HTTP Method", + "de_DE": "HTTP-Methode", + }, + Help: map[string]string{ + "en_US": "HTTP request method used for the web request.", + "de_DE": "HTTP-Methode für die Anfrage.", + }, + Default: "POST", + Required: true, + }, + { + Name: "url_template", + Type: "string", + Label: map[string]string{ + "en_US": "URL Template", + "de_DE": "URL-Template", + }, + Help: map[string]string{ + "en_US": "URL, optionally as a Go template over the current plugin.NotificationRequest.", + "de_DE": "URL, optional als Go-Template über das zu verarbeitende plugin.NotificationRequest.", + }, + Required: true, + }, + { + Name: "request_body_template", + Type: "string", + Label: map[string]string{ + "en_US": "Request Body Template", + "de_DE": "Anfragedaten-Template", + }, + Help: map[string]string{ + "en_US": "Go template applied to the current plugin.NotificationRequest to create an request body.", + "de_DE": "Go-Template über das zu verarbeitende plugin.NotificationRequest zum Erzeugen der mitgesendeten Anfragedaten.", + }, + Default: `{{json .}}`, + }, + { + Name: "response_status_codes", + Type: "string", + Label: map[string]string{ + "en_US": "Response Status Codes", + "de_DE": "Antwort-Status-Codes", + }, + Help: map[string]string{ + "en_US": "Comma separated list of expected HTTP response status code, e.g., 200,201,202,208,418", + "de_DE": "Kommaseparierte Liste erwarteter Status-Code der HTTP-Antwort, z.B.: 200,201,202,208,418", + }, + Default: "200", + Required: true, + }, + } + + configAttrs, err := json.Marshal(elements) + if err != nil { + panic(err) + } + + return &plugin.Info{ + Name: "Webhook", + Version: internal.Version.Version, + Author: "Icinga GmbH", + ConfigAttributes: configAttrs, + } +} + +func (ch *Webhook) SetConfig(jsonStr json.RawMessage) error { + err := json.Unmarshal(jsonStr, ch) + if err != nil { + return err + } + + tmplFuncs := template.FuncMap{ + "json": func(a any) (string, error) { + data, err := json.Marshal(a) + if err != nil { + return "", err + } + return string(data), nil + + }, + } + + ch.tmplUrl, err = template.New("url").Funcs(tmplFuncs).Parse(ch.URLTemplate) + if err != nil { + return fmt.Errorf("cannot parse URL template: %w", err) + } + + ch.tmplRequestBody, err = template.New("request_body").Funcs(tmplFuncs).Parse(ch.RequestBodyTemplate) + if err != nil { + return fmt.Errorf("cannot parse Request Body template: %w", err) + } + + respStatusCodes := strings.Split(ch.ResponseStatusCodes, ",") + ch.respStatusCodes = make([]int, len(respStatusCodes)) + for i, respStatusCodeStr := range respStatusCodes { + respStatusCode, err := strconv.Atoi(respStatusCodeStr) + if err != nil { + return fmt.Errorf("cannot convert status code %q to int: %w", respStatusCodeStr, err) + } + ch.respStatusCodes[i] = respStatusCode + } + + return nil +} + +func (ch *Webhook) SendNotification(req *plugin.NotificationRequest) error { + var urlBuff, reqBodyBuff bytes.Buffer + if err := ch.tmplUrl.Execute(&urlBuff, req); err != nil { + return fmt.Errorf("cannot execute URL template: %w", err) + } + if err := ch.tmplRequestBody.Execute(&reqBodyBuff, req); err != nil { + return fmt.Errorf("cannot execute Request Body template: %w", err) + } + + httpReq, err := http.NewRequest(ch.Method, urlBuff.String(), &reqBodyBuff) + if err != nil { + return err + } + httpResp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return err + } + _, _ = io.Copy(io.Discard, httpResp.Body) + _ = httpResp.Body.Close() + + if !slices.Contains(ch.respStatusCodes, httpResp.StatusCode) { + return fmt.Errorf("unaccepted HTTP response status code %d not in %v", + httpResp.StatusCode, ch.respStatusCodes) + } + + return nil +} + +func main() { + plugin.RunPlugin(&Webhook{}) +}