From 967124eb43ecda72f9f893fa837cf3afa76471e4 Mon Sep 17 00:00:00 2001 From: TwiN Date: Tue, 8 Nov 2022 20:47:39 -0500 Subject: [PATCH] fix(alerting): Resolve GoogleChat issue with bad payload when condition has `"` in it Fixes #362 --- alerting/config.go | 2 +- alerting/provider/googlechat/googlechat.go | 174 +++++++++++++----- .../provider/googlechat/googlechat_test.go | 16 +- 3 files changed, 138 insertions(+), 54 deletions(-) diff --git a/alerting/config.go b/alerting/config.go index 106b2e33..90f7dee7 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -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 diff --git a/alerting/provider/googlechat/googlechat.go b/alerting/provider/googlechat/googlechat.go index 99b62ae8..79ea4439 100644 --- a/alerting/provider/googlechat/googlechat.go +++ b/alerting/provider/googlechat/googlechat.go @@ -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": [ - { - "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) + body, _ := json.Marshal(Body{ + Cards: []Cards{ + { + Sections: []Sections{ + { + Widgets: []Widgets{ + { + KeyValue: KeyValue{ + TopLabel: endpoint.DisplayName(), + Content: message, + ContentMultiline: "true", + BottomLabel: description, + Icon: "BOOKMARK", + }, + }, + { + KeyValue: KeyValue{ + TopLabel: "Condition results", + Content: results, + ContentMultiline: "true", + Icon: "DESCRIPTION", + }, + }, + { + Buttons: []Buttons{ + { + 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 diff --git a/alerting/provider/googlechat/googlechat_test.go b/alerting/provider/googlechat/googlechat_test.go index 3fd7956e..ed7de1a2 100644 --- a/alerting/provider/googlechat/googlechat_test.go +++ b/alerting/provider/googlechat/googlechat_test.go @@ -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()) } })