From da24b7e8ac4276c8abb2249fdd099f7d32cdc5ed Mon Sep 17 00:00:00 2001 From: TwiN Date: Thu, 20 Oct 2022 14:45:42 -0400 Subject: [PATCH] fix(alerting): Resolve Slack issue with bad payload when condition has `"` in it --- alerting/provider/slack/slack.go | 68 ++++++++++++++++++--------- alerting/provider/slack/slack_test.go | 12 ++--- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go index b0a74cb6..dcee7d6b 100644 --- a/alerting/provider/slack/slack.go +++ b/alerting/provider/slack/slack.go @@ -2,6 +2,7 @@ package slack import ( "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -59,9 +60,28 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, return err } +type Body struct { + Text string `json:"text"` + Attachments []Attachment `json:"attachments"` +} + +type Attachment struct { + Title string `json:"title"` + Text string `json:"text"` + Short bool `json:"short"` + Color string `json:"color"` + Fields []Field `json:"fields"` +} + +type Field struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + // buildRequestBody builds the request body for the provider -func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { - var message, color, results string +func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { + var message, color string if resolved { message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) color = "#36A64F" @@ -69,37 +89,39 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) color = "#DD0000" } - for _, conditionResult := range result.ConditionResults { + fields := make([]Field, len(result.ConditionResults)) + for i, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { prefix = ":white_check_mark:" } else { prefix = ":x:" } - results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition) + fields[i] = Field{ + Value: fmt.Sprintf("%s - `%s`", prefix, conditionResult.Condition), + Short: false, + } + if i == 0 { + fields[i].Title = "Condition results" + } } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { - description = ":\\n> " + alertDescription + description = ":\n> " + alertDescription } - return fmt.Sprintf(`{ - "text": "", - "attachments": [ - { - "title": ":helmet_with_white_cross: Gatus", - "text": "%s%s", - "short": false, - "color": "%s", - "fields": [ - { - "title": "Condition results", - "value": "%s", - "short": false - } - ] - } - ] -}`, message, description, color, results) + body, _ := json.Marshal(Body{ + Text: "", + Attachments: []Attachment{ + { + Title: ":helmet_with_white_cross: Gatus", + Text: message + description, + Short: false, + Color: color, + Fields: fields, + }, + }, + }) + return body } // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group diff --git a/alerting/provider/slack/slack_test.go b/alerting/provider/slack/slack_test.go index 74014ced..64f96f30 100644 --- a/alerting/provider/slack/slack_test.go +++ b/alerting/provider/slack/slack_test.go @@ -153,7 +153,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Endpoint: core.Endpoint{Name: "name"}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: false, - ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"short\": false,\n \"color\": \"#DD0000\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}", + ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\",\"short\":false},{\"title\":\"\",\"value\":\":x: - `[STATUS] == 200`\",\"short\":false}]}]}", }, { Name: "triggered-with-group", @@ -161,7 +161,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Endpoint: core.Endpoint{Name: "name", Group: "group"}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: false, - ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"short\": false,\n \"color\": \"#DD0000\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}", + ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\",\"short\":false},{\"title\":\"\",\"value\":\":x: - `[STATUS] == 200`\",\"short\":false}]}]}", }, { Name: "resolved", @@ -169,7 +169,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Endpoint: core.Endpoint{Name: "name"}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: true, - ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"short\": false,\n \"color\": \"#36A64F\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}", + ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\",\"short\":false},{\"title\":\"\",\"value\":\":white_check_mark: - `[STATUS] == 200`\",\"short\":false}]}]}", }, { Name: "resolved-with-group", @@ -177,7 +177,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Endpoint: core.Endpoint{Name: "name", Group: "group"}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: true, - ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"short\": false,\n \"color\": \"#36A64F\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}", + ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\",\"short\":false},{\"title\":\"\",\"value\":\":white_check_mark: - `[STATUS] == 200`\",\"short\":false}]}]}", }, } for _, scenario := range scenarios { @@ -193,11 +193,11 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { }, scenario.Resolved, ) - if body != scenario.ExpectedBody { + if string(body) != scenario.ExpectedBody { t.Errorf("expected %s, got %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()) } })