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 *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"`
// Discord is the configuration for the discord alerting provider

View File

@ -2,6 +2,7 @@ package googlechat
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
@ -52,7 +53,7 @@ func (provider *AlertProvider) IsValid() bool {
// Send an alert using the provider
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)
if err != nil {
return err
@ -70,8 +71,50 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
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
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
if resolved {
color = "#36A64F"
@ -94,49 +137,90 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":: " + alertDescription
}
return fmt.Sprintf(`{
"cards": [
body, _ := json.Marshal(Body{
Cards: []Cards{
{
"sections": [
Sections: []Sections{
{
"widgets": [
Widgets: []Widgets{
{
"keyValue": {
"topLabel": "%s [%s]",
"content": "%s",
"contentMultiline": "true",
"bottomLabel": "%s",
"icon": "BOOKMARK"
}
KeyValue: KeyValue{
TopLabel: endpoint.DisplayName(),
Content: message,
ContentMultiline: "true",
BottomLabel: description,
Icon: "BOOKMARK",
},
},
{
"keyValue": {
"topLabel": "Condition results",
"content": "%s",
"contentMultiline": "true",
"icon": "DESCRIPTION"
}
KeyValue: KeyValue{
TopLabel: "Condition results",
Content: results,
ContentMultiline: "true",
Icon: "DESCRIPTION",
},
},
{
"buttons": [
Buttons: []Buttons{
{
"textButton": {
"text": "URL",
"onClick": {
"openLink": {
"url": "%s"
}
}
}
}
]
}
]
}
]
}
]
}`, endpoint.Name, endpoint.Group, message, description, results, endpoint.URL)
TextButton: TextButton{
Text: "Open",
OnClick: OnClick{OpenLink: OpenLink{URL: endpoint.URL}},
},
},
},
},
},
},
},
},
},
})
return body
// return fmt.Sprintf(`{
// "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

View File

@ -151,20 +151,20 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
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",
Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
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 {
t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"},
&core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
&scenario.Alert,
&core.Result{
ConditionResults: []*core.ConditionResult{
@ -174,13 +174,13 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
},
scenario.Resolved,
)
b, _ := json.Marshal(body)
e, _ := json.Marshal(scenario.ExpectedBody)
if body != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", e, b)
//b, _ := json.Marshal(body)
//e, _ := json.Marshal(scenario.ExpectedBody)
if string(body) != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
}
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())
}
})