fix(alerting): Resolve GoogleChat issue with bad payload when condition has " in it

Fixes #362
This commit is contained in:
TwiN 2022-11-08 20:47:39 -05:00
parent fa47a199e5
commit 967124eb43
3 changed files with 138 additions and 54 deletions

View File

@ -24,7 +24,7 @@ type Config struct {
// Custom is the configuration for the custom alerting provider // Custom is the configuration for the custom alerting provider
Custom *custom.AlertProvider `yaml:"custom,omitempty"` Custom *custom.AlertProvider `yaml:"custom,omitempty"`
// googlechat is the configuration for the Google chat alerting provider // GoogleChat is the configuration for the Google chat alerting provider
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"` GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
// Discord is the configuration for the discord alerting provider // Discord is the configuration for the discord alerting provider

View File

@ -2,6 +2,7 @@ package googlechat
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -52,7 +53,7 @@ func (provider *AlertProvider) IsValid() bool {
// Send an alert using the provider // Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved))) buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer) request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
if err != nil { if err != nil {
return err return err
@ -70,8 +71,50 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
return err return err
} }
type Body struct {
Cards []Cards `json:"cards"`
}
type Cards struct {
Sections []Sections `json:"sections"`
}
type Sections struct {
Widgets []Widgets `json:"widgets"`
}
type Widgets struct {
KeyValue KeyValue `json:"keyValue,omitempty"`
Buttons []Buttons `json:"buttons,omitempty"`
}
type KeyValue struct {
TopLabel string `json:"topLabel"`
Content string `json:"content"`
ContentMultiline string `json:"contentMultiline"`
BottomLabel string `json:"bottomLabel"`
Icon string `json:"icon"`
}
type Buttons struct {
TextButton TextButton `json:"textButton"`
}
type TextButton struct {
Text string `json:"text"`
OnClick OnClick `json:"onClick"`
}
type OnClick struct {
OpenLink OpenLink `json:"openLink"`
}
type OpenLink struct {
URL string `json:"url"`
}
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
var message, color string var message, color string
if resolved { if resolved {
color = "#36A64F" color = "#36A64F"
@ -94,49 +137,90 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":: " + alertDescription description = ":: " + alertDescription
} }
return fmt.Sprintf(`{ body, _ := json.Marshal(Body{
"cards": [ Cards: []Cards{
{ {
"sections": [ Sections: []Sections{
{ {
"widgets": [ Widgets: []Widgets{
{ {
"keyValue": { KeyValue: KeyValue{
"topLabel": "%s [%s]", TopLabel: endpoint.DisplayName(),
"content": "%s", Content: message,
"contentMultiline": "true", ContentMultiline: "true",
"bottomLabel": "%s", BottomLabel: description,
"icon": "BOOKMARK" Icon: "BOOKMARK",
} },
}, },
{ {
"keyValue": { KeyValue: KeyValue{
"topLabel": "Condition results", TopLabel: "Condition results",
"content": "%s", Content: results,
"contentMultiline": "true", ContentMultiline: "true",
"icon": "DESCRIPTION" Icon: "DESCRIPTION",
} },
}, },
{ {
"buttons": [ Buttons: []Buttons{
{ {
"textButton": { TextButton: TextButton{
"text": "URL", Text: "Open",
"onClick": { OnClick: OnClick{OpenLink: OpenLink{URL: endpoint.URL}},
"openLink": { },
"url": "%s" },
} },
} },
} },
} },
] },
} },
] },
} })
] return body
}
] // return fmt.Sprintf(`{
}`, endpoint.Name, endpoint.Group, message, description, results, endpoint.URL) // "cards": [
// {
// "sections": [
// {
// "widgets": [
// {
// "keyValue": {
// "topLabel": "%s [%s]",
// "content": "%s",
// "contentMultiline": "true",
// "bottomLabel": "%s",
// "icon": "BOOKMARK"
// }
// },
// {
// "keyValue": {
// "topLabel": "Condition results",
// "content": "%s",
// "contentMultiline": "true",
// "icon": "DESCRIPTION"
// }
// },
// {
// "buttons": [
// {
// "textButton": {
// "text": "URL",
// "onClick": {
// "openLink": {
// "url": "%s"
// }
// }
// }
// }
// ]
// }
// ]
// }
// ]
// }
//]
//}`, endpoint.Name, endpoint.Group, message, description, results, endpoint.URL)
} }
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group

View File

@ -151,20 +151,20 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false, Resolved: false,
ExpectedBody: "{\n \"cards\": [\n {\n \"sections\": [\n {\n \"widgets\": [\n {\n \"keyValue\": {\n \"topLabel\": \"endpoint-name []\",\n \"content\": \"\u003cfont color='#DD0000'\u003eAn alert has been triggered due to having failed 3 time(s) in a row\u003c/font\u003e\",\n \"contentMultiline\": \"true\",\n \"bottomLabel\": \":: description-1\",\n \"icon\": \"BOOKMARK\"\n }\n },\n {\n \"keyValue\": {\n \"topLabel\": \"Condition results\",\n \"content\": \"❌ [CONNECTED] == true\u003cbr\u003e❌ [STATUS] == 200\u003cbr\u003e\",\n \"contentMultiline\": \"true\",\n \"icon\": \"DESCRIPTION\"\n }\n },\n {\n \"buttons\": [\n {\n \"textButton\": {\n \"text\": \"URL\",\n \"onClick\": {\n \"openLink\": {\n \"url\": \"\"\n }\n }\n }\n }\n ]\n }\n ]\n }\n ]\n }\n]\n}", ExpectedBody: `{"cards":[{"sections":[{"widgets":[{"keyValue":{"topLabel":"endpoint-name","content":"\u003cfont color='#DD0000'\u003eAn alert has been triggered due to having failed 3 time(s) in a row\u003c/font\u003e","contentMultiline":"true","bottomLabel":":: description-1","icon":"BOOKMARK"}},{"keyValue":{"topLabel":"Condition results","content":"❌ [CONNECTED] == true\u003cbr\u003e❌ [STATUS] == 200\u003cbr\u003e","contentMultiline":"true","bottomLabel":"","icon":"DESCRIPTION"}},{"keyValue":{"topLabel":"","content":"","contentMultiline":"","bottomLabel":"","icon":""},"buttons":[{"textButton":{"text":"Open","onClick":{"openLink":{"url":"https://example.org"}}}}]}]}]}]}`,
}, },
{ {
Name: "resolved", Name: "resolved",
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true, Resolved: true,
ExpectedBody: "{\n \"cards\": [\n {\n \"sections\": [\n {\n \"widgets\": [\n {\n \"keyValue\": {\n \"topLabel\": \"endpoint-name []\",\n \"content\": \"\u003cfont color='#36A64F'\u003eAn alert has been resolved after passing successfully 5 time(s) in a row\u003c/font\u003e\",\n \"contentMultiline\": \"true\",\n \"bottomLabel\": \":: description-2\",\n \"icon\": \"BOOKMARK\"\n }\n },\n {\n \"keyValue\": {\n \"topLabel\": \"Condition results\",\n \"content\": \"✅ [CONNECTED] == true\u003cbr\u003e✅ [STATUS] == 200\u003cbr\u003e\",\n \"contentMultiline\": \"true\",\n \"icon\": \"DESCRIPTION\"\n }\n },\n {\n \"buttons\": [\n {\n \"textButton\": {\n \"text\": \"URL\",\n \"onClick\": {\n \"openLink\": {\n \"url\": \"\"\n }\n }\n }\n }\n ]\n }\n ]\n }\n ]\n }\n]\n}", ExpectedBody: `{"cards":[{"sections":[{"widgets":[{"keyValue":{"topLabel":"endpoint-name","content":"\u003cfont color='#36A64F'\u003eAn alert has been resolved after passing successfully 5 time(s) in a row\u003c/font\u003e","contentMultiline":"true","bottomLabel":":: description-2","icon":"BOOKMARK"}},{"keyValue":{"topLabel":"Condition results","content":"✅ [CONNECTED] == true\u003cbr\u003e✅ [STATUS] == 200\u003cbr\u003e","contentMultiline":"true","bottomLabel":"","icon":"DESCRIPTION"}},{"keyValue":{"topLabel":"","content":"","contentMultiline":"","bottomLabel":"","icon":""},"buttons":[{"textButton":{"text":"Open","onClick":{"openLink":{"url":"https://example.org"}}}}]}]}]}]}`,
}, },
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &core.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: []*core.ConditionResult{
@ -174,13 +174,13 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
}, },
scenario.Resolved, scenario.Resolved,
) )
b, _ := json.Marshal(body) //b, _ := json.Marshal(body)
e, _ := json.Marshal(scenario.ExpectedBody) //e, _ := json.Marshal(scenario.ExpectedBody)
if body != scenario.ExpectedBody { if string(body) != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", e, b) t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
} }
out := make(map[string]interface{}) out := make(map[string]interface{})
if err := json.Unmarshal([]byte(body), &out); err != nil { if err := json.Unmarshal(body, &out); err != nil {
t.Error("expected body to be valid JSON, got error:", err.Error()) t.Error("expected body to be valid JSON, got error:", err.Error())
} }
}) })