diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index 634569dd9bc..86744fc3b17 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -1,12 +1,15 @@ package channels import ( + "bytes" "context" "encoding/json" + "fmt" "github.com/pkg/errors" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" + "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -14,18 +17,194 @@ import ( "github.com/grafana/grafana/pkg/services/notifications" ) -// TeamsNotifier is responsible for sending -// alert notifications to Microsoft teams. -type TeamsNotifier struct { - *Base - URL string - Message string - Title string - SectionTitle string - tmpl *template.Template - log log.Logger - ns notifications.WebhookSender - images ImageStore +const ( + ImageSizeSmall = "small" + ImageSizeMedium = "medium" + ImageSizeLarge = "large" + + TextColorDark = "dark" + TextColorLight = "light" + TextColorAccent = "accent" + TextColorGood = "good" + TextColorWarning = "warning" + TextColorAttention = "attention" + + TextSizeSmall = "small" + TextSizeMedium = "medium" + TextSizeLarge = "large" + TextSizeExtraLarge = "extraLarge" + TextSizeDefault = "default" + + TextWeightLighter = "lighter" + TextWeightBolder = "bolder" + TextWeightDefault = "default" +) + +// AdaptiveCardsMessage represents a message for adaptive cards. +type AdaptiveCardsMessage struct { + Attachments []AdaptiveCardsAttachment `json:"attachments"` + Type string `json:"type"` +} + +// NewAdaptiveCardsMessage returns a message prepared for adaptive cards. +// https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#send-adaptive-cards-using-an-incoming-webhook +func NewAdaptiveCardsMessage(card AdaptiveCard) AdaptiveCardsMessage { + return AdaptiveCardsMessage{ + Attachments: []AdaptiveCardsAttachment{{ + ContentType: "application/vnd.microsoft.card.adaptive", + Content: card, + }}, + Type: "message", + } +} + +// AdaptiveCardsAttachment contains an adaptive card. +type AdaptiveCardsAttachment struct { + Content AdaptiveCard `json:"content"` + ContentType string `json:"contentType"` + ContentURL string `json:"contentUrl,omitempty"` +} + +// AdapativeCard repesents an Adaptive Card. +// https://adaptivecards.io/explorer/AdaptiveCard.html +type AdaptiveCard struct { + Body []AdaptiveCardItem `json:"body"` + Schema string `json:"$schema"` + Type string `json:"type"` + Version string `json:"version"` +} + +// NewAdaptiveCard returns a prepared Adaptive Card. +func NewAdaptiveCard() AdaptiveCard { + return AdaptiveCard{ + Body: make([]AdaptiveCardItem, 0), + Schema: "http://adaptivecards.io/schemas/adaptive-card.json", + Type: "AdaptiveCard", + Version: "1.4", + } +} + +// AppendItem appends an item, such as text or an image, to the Adaptive Card. +func (c *AdaptiveCard) AppendItem(i AdaptiveCardItem) { + c.Body = append(c.Body, i) +} + +// AdaptiveCardItem is an interface for adaptive card items such as containers, elements and inputs. +type AdaptiveCardItem interface { + MarshalJSON() ([]byte, error) +} + +// AdaptiveCardTextBlockItem is a TextBlock. +type AdaptiveCardTextBlockItem struct { + Color string + Size string + Text string + Weight string + Wrap bool +} + +func (i AdaptiveCardTextBlockItem) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + Text string `json:"text"` + Color string `json:"color,omitempty"` + Size string `json:"size,omitempty"` + Weight string `json:"weight,omitempty"` + Wrap bool `json:"wrap,omitempty"` + }{ + Type: "TextBlock", + Text: i.Text, + Color: i.Color, + Size: i.Size, + Weight: i.Weight, + Wrap: i.Wrap, + }) +} + +// AdaptiveCardImageSetItem is an ImageSet. +type AdaptiveCardImageSetItem struct { + Images []AdaptiveCardImageItem + Size string +} + +// AppendImage appends an image to image set. +func (i *AdaptiveCardImageSetItem) AppendImage(image AdaptiveCardImageItem) { + i.Images = append(i.Images, image) +} + +func (i AdaptiveCardImageSetItem) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + Images []AdaptiveCardImageItem `json:"images"` + Size string `json:"imageSize"` + }{ + Type: "ImageSet", + Images: i.Images, + Size: i.Size, + }) +} + +// AdaptiveCardImageItem is an Image. +type AdaptiveCardImageItem struct { + AltText string + Size string + URL string +} + +func (i AdaptiveCardImageItem) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + URL string `json:"url"` + AltText string `json:"altText,omitempty"` + Size string `json:"size,omitempty"` + MsTeams map[string]interface{} `json:"msTeams,omitempty"` + }{ + Type: "Image", + URL: i.URL, + AltText: i.AltText, + Size: i.Size, + MsTeams: map[string]interface{}{"allowExpand": true}, + }) +} + +// AdaptiveCardActionSetItem is an ActionSet. +type AdaptiveCardActionSetItem struct { + Actions []AdaptiveCardActionItem +} + +func (i AdaptiveCardActionSetItem) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + Actions []AdaptiveCardActionItem `json:"actions"` + }{ + Type: "ActionSet", + Actions: i.Actions, + }) +} + +type AdaptiveCardActionItem interface { + MarshalJSON() ([]byte, error) +} + +// AdapativeCardOpenURLActionItem is an Action.OpenUrl action. +type AdaptiveCardOpenURLActionItem struct { + IconURL string + Title string + URL string +} + +func (i AdaptiveCardOpenURLActionItem) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + Title string `json:"title"` + URL string `json:"url"` + IconURL string `json:"iconUrl,omitempty"` + }{ + Type: "Action.OpenUrl", + Title: i.Title, + URL: i.URL, + IconURL: i.IconURL, + }) } type TeamsConfig struct { @@ -36,17 +215,6 @@ type TeamsConfig struct { SectionTitle string } -func TeamsFactory(fc FactoryConfig) (NotificationChannel, error) { - cfg, err := NewTeamsConfig(fc.Config) - if err != nil { - return nil, receiverInitError{ - Reason: err.Error(), - Cfg: *fc.Config, - } - } - return NewTeamsNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil -} - func NewTeamsConfig(config *NotificationChannelConfig) (*TeamsConfig, error) { URL := config.Settings.Get("url").MustString() if URL == "" { @@ -61,8 +229,16 @@ func NewTeamsConfig(config *NotificationChannelConfig) (*TeamsConfig, error) { }, nil } -type teamsImage struct { - Image string `json:"image"` +type TeamsNotifier struct { + *Base + URL string + Message string + Title string + SectionTitle string + tmpl *template.Template + log log.Logger + ns notifications.WebhookSender + images ImageStore } // NewTeamsNotifier is the constructor for Teams notifier. @@ -86,60 +262,62 @@ func NewTeamsNotifier(config *TeamsConfig, ns notifications.WebhookSender, image } } -// Notify send an alert notification to Microsoft teams. +func TeamsFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewTeamsConfig(fc.Config) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewTeamsNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil +} + func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { var tmplErr error tmpl, _ := TmplText(ctx, tn.tmpl, as, tn.log, &tmplErr) - ruleURL := joinUrlPath(tn.tmpl.ExternalURL.String(), "/alerting/list", tn.log) + card := NewAdaptiveCard() + card.AppendItem(AdaptiveCardTextBlockItem{ + Color: getTeamsTextColor(types.Alerts(as...)), + Text: tmpl(tn.Title), + Size: TextSizeLarge, + Weight: TextWeightBolder, + Wrap: true, + }) + card.AppendItem(AdaptiveCardTextBlockItem{ + Text: tmpl(tn.Message), + Wrap: true, + }) - var images []teamsImage + var s AdaptiveCardImageSetItem _ = withStoredImages(ctx, tn.log, tn.images, func(_ int, image ngmodels.Image) error { - if len(image.URL) != 0 { - images = append(images, teamsImage{Image: image.URL}) + if image.URL != "" { + s.AppendImage(AdaptiveCardImageItem{URL: image.URL}) } return nil }, as...) - // Note: these template calls must remain in this order - title := tmpl(tn.Title) - sections := []map[string]interface{}{ - { - "title": tmpl(tn.SectionTitle), - "text": tmpl(tn.Message), - }, + if len(s.Images) > 2 { + s.Size = ImageSizeMedium + card.AppendItem(s) + } else if len(s.Images) > 0 { + s.Size = ImageSizeLarge + card.AppendItem(s) } - if len(images) != 0 { - sections[0]["images"] = images - } - - body := map[string]interface{}{ - "@type": "MessageCard", - "@context": "http://schema.org/extensions", - // summary MUST not be empty or the webhook request fails - // summary SHOULD contain some meaningful information, since it is used for mobile notifications - "summary": title, - "title": title, - "themeColor": getAlertStatusColor(types.Alerts(as...).Status()), - "sections": sections, - "potentialAction": []map[string]interface{}{ - { - "@context": "http://schema.org", - "@type": "OpenUri", - "name": "View Rule", - "targets": []map[string]interface{}{ - { - "os": "default", - "uri": ruleURL, - }, - }, + card.AppendItem(AdaptiveCardActionSetItem{ + Actions: []AdaptiveCardActionItem{ + AdaptiveCardOpenURLActionItem{ + Title: "View URL", + URL: joinUrlPath(tn.tmpl.ExternalURL.String(), "/alerting/list", tn.log), }, }, - } + }) + // This check for tmplErr must happen before templating the URL if tmplErr != nil { tn.log.Warn("failed to template Teams message", "err", tmplErr.Error()) tmplErr = nil @@ -151,23 +329,20 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, u = tn.URL } - b, err := json.Marshal(&body) + b, err := json.Marshal(NewAdaptiveCardsMessage(card)) if err != nil { - return false, errors.Wrap(err, "marshal json") + return false, fmt.Errorf("failed to marshal JSON: %w", err) } + cmd := &models.SendWebhookSync{Url: u, Body: string(b)} - - // Teams does not always return non-2xx response when the request fails. Instead, the response body can contain an error message regardless of status code. - // Ex. 429 - Too Many Requests: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors + // Teams sometimes does not use status codes to show when a request has failed. Instead, the + // response can contain an error message, irrespective of status code (i.e. https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors) cmd.Validation = func(b []byte, statusCode int) error { - body := string(b) - + // The request succeeded if the response is "1" // https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#send-messages-using-curl-and-powershell - // Above states that if the POST succeeds, you must see a simple "1" output. - if body != "1" { - return errors.New(body) + if !bytes.Equal(b, []byte("1")) { + return errors.New(string(b)) } - return nil } @@ -181,3 +356,11 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, func (tn *TeamsNotifier) SendResolved() bool { return !tn.GetDisableResolveMessage() } + +// getTeamsTextColor returns the text color for the message title. +func getTeamsTextColor(alerts model.Alerts) string { + if getAlertStatusColor(alerts.Status()) == ColorAlertFiring { + return TextColorAttention + } + return TextColorGood +} diff --git a/pkg/services/ngalert/notifier/channels/teams_test.go b/pkg/services/ngalert/notifier/channels/teams_test.go index c52f805dc93..152b4f872bc 100644 --- a/pkg/services/ngalert/notifier/channels/teams_test.go +++ b/pkg/services/ngalert/notifier/channels/teams_test.go @@ -34,193 +34,226 @@ func TestTeamsNotifier(t *testing.T) { expMsg map[string]interface{} expInitError string expMsgError error - }{ - { - name: "Default config with one alert", - settings: `{"url": "http://localhost"}`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, - }, + }{{ + name: "Default config with one alert", + settings: `{"url": "http://localhost"}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, }, }, - expMsg: map[string]interface{}{ - "@type": "MessageCard", - "@context": "http://schema.org/extensions", - "summary": "[FIRING:1] (val1)", - "title": "[FIRING:1] (val1)", - "themeColor": "#D63232", - "sections": []map[string]interface{}{ - { - "title": "", - "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", - }, + }, + expMsg: map[string]interface{}{ + "attachments": []map[string]interface{}{{ + "content": map[string]interface{}{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": []map[string]interface{}{{ + "color": "attention", + "size": "large", + "text": "[FIRING:1] (val1)", + "type": "TextBlock", + "weight": "bolder", + "wrap": true, + }, { + "text": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "type": "TextBlock", + "wrap": true, + }, { + "actions": []map[string]interface{}{{ + "title": "View URL", + "type": "Action.OpenUrl", + "url": "http://localhost/alerting/list", + }}, + "type": "ActionSet", + }}, + "type": "AdaptiveCard", + "version": "1.4", }, - "potentialAction": []map[string]interface{}{ - { - "@context": "http://schema.org", - "@type": "OpenUri", - "name": "View Rule", - "targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}}, - }, + "contentType": "application/vnd.microsoft.card.adaptive", + }}, + "type": "message", + }, + expMsgError: nil, + }, { + name: "Custom config with multiple alerts", + settings: `{ + "url": "http://localhost", + "title": "{{ .CommonLabels.alertname }}", + "sectiontitle": "Details", + "message": "{{ len .Alerts.Firing }} alerts are firing, {{ len .Alerts.Resolved }} are resolved" +}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + }, + }, { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, + Annotations: model.LabelSet{"ann1": "annv2"}, }, }, - expMsgError: nil, - }, { - name: "Custom config with multiple alerts", - settings: `{ - "url": "http://localhost", - "title": "{{ .CommonLabels.alertname }}", - "sectiontitle": "Details", - "message": "{{ len .Alerts.Firing }} alerts are firing, {{ len .Alerts.Resolved }} are resolved" - }`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1"}, - }, - }, { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, - Annotations: model.LabelSet{"ann1": "annv2"}, - }, + }, + expMsg: map[string]interface{}{ + "attachments": []map[string]interface{}{{ + "content": map[string]interface{}{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": []map[string]interface{}{{ + "color": "attention", + "size": "large", + "text": "alert1", + "type": "TextBlock", + "weight": "bolder", + "wrap": true, + }, { + "text": "2 alerts are firing, 0 are resolved", + "type": "TextBlock", + "wrap": true, + }, { + "actions": []map[string]interface{}{{ + "title": "View URL", + "type": "Action.OpenUrl", + "url": "http://localhost/alerting/list", + }}, + "type": "ActionSet", + }}, + "type": "AdaptiveCard", + "version": "1.4", + }, + "contentType": "application/vnd.microsoft.card.adaptive", + }}, + "type": "message", + }, + expMsgError: nil, + }, { + name: "Missing field in template", + settings: `{ + "url": "http://localhost", + "title": "{{ .CommonLabels.alertname }}", + "sectiontitle": "Details", + "message": "I'm a custom template {{ .NotAField }} bad template" +}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + }, + }, { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, + Annotations: model.LabelSet{"ann1": "annv2"}, }, }, - expMsg: map[string]interface{}{ - "@type": "MessageCard", - "@context": "http://schema.org/extensions", - "summary": "alert1", - "title": "alert1", - "themeColor": "#D63232", - "sections": []map[string]interface{}{ - { - "title": "Details", - "text": "2 alerts are firing, 0 are resolved", - }, + }, + expMsg: map[string]interface{}{ + "attachments": []map[string]interface{}{{ + "content": map[string]interface{}{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": []map[string]interface{}{{ + "color": "attention", + "size": "large", + "text": "alert1", + "type": "TextBlock", + "weight": "bolder", + "wrap": true, + }, { + "text": "I'm a custom template ", + "type": "TextBlock", + "wrap": true, + }, { + "actions": []map[string]interface{}{{ + "title": "View URL", + "type": "Action.OpenUrl", + "url": "http://localhost/alerting/list", + }}, + "type": "ActionSet", + }}, + "type": "AdaptiveCard", + "version": "1.4", }, - "potentialAction": []map[string]interface{}{ - { - "@context": "http://schema.org", - "@type": "OpenUri", - "name": "View Rule", - "targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}}, - }, - }, - }, - expMsgError: nil, - }, { - name: "Missing field in template", - settings: `{ - "url": "http://localhost", - "title": "{{ .CommonLabels.alertname }}", - "sectiontitle": "Details", - "message": "I'm a custom template {{ .NotAField }} bad template" - }`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1"}, - }, - }, { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, - Annotations: model.LabelSet{"ann1": "annv2"}, - }, - }, - }, - expMsg: map[string]interface{}{ - "@type": "MessageCard", - "@context": "http://schema.org/extensions", - "summary": "alert1", - "title": "alert1", - "themeColor": "#D63232", - "sections": []map[string]interface{}{ - { - "title": "Details", - "text": "I'm a custom template ", - }, - }, - "potentialAction": []map[string]interface{}{ - { - "@context": "http://schema.org", - "@type": "OpenUri", - "name": "View Rule", - "targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}}, - }, - }, - }, - expMsgError: nil, - }, { - name: "Invalid template", - settings: `{ + "contentType": "application/vnd.microsoft.card.adaptive", + }}, + "type": "message", + }, + expMsgError: nil, + }, { + name: "Invalid template", + settings: `{ "url": "http://localhost", "title": "{{ .CommonLabels.alertname }}", "sectiontitle": "Details", "message": "I'm a custom template {{ {.NotAField }} bad template" }`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1"}, - }, - }, { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, - Annotations: model.LabelSet{"ann1": "annv2"}, - }, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + }, + }, { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, + Annotations: model.LabelSet{"ann1": "annv2"}, }, }, - expMsg: map[string]interface{}{ - "@type": "MessageCard", - "@context": "http://schema.org/extensions", - "summary": "alert1", - "title": "alert1", - "themeColor": "#D63232", - "sections": []map[string]interface{}{ - { - "title": "Details", - "text": "", - }, - }, - "potentialAction": []map[string]interface{}{ - { - "@context": "http://schema.org", - "@type": "OpenUri", - "name": "View Rule", - "targets": []map[string]interface{}{{"os": "default", "uri": "http://localhost/alerting/list"}}, - }, - }, - }, - expMsgError: nil, - }, { - name: "Error in initing", - settings: `{}`, - expInitError: `could not find url property in settings`, }, - { - name: "webhook returns error message in body with 200", - settings: `{"url": "http://localhost"}`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, - }, + expMsg: map[string]interface{}{ + "attachments": []map[string]interface{}{{ + "content": map[string]interface{}{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": []map[string]interface{}{{ + "color": "attention", + "size": "large", + "text": "alert1", + "type": "TextBlock", + "weight": "bolder", + "wrap": true, + }, { + "text": "", + "type": "TextBlock", + "wrap": true, + }, { + "actions": []map[string]interface{}{{ + "title": "View URL", + "type": "Action.OpenUrl", + "url": "http://localhost/alerting/list", + }}, + "type": "ActionSet", + }}, + "type": "AdaptiveCard", + "version": "1.4", + }, + "contentType": "application/vnd.microsoft.card.adaptive", + }}, + "type": "message", + }, + expMsgError: nil, + }, { + name: "Error in initing", + settings: `{}`, + expInitError: `could not find url property in settings`, + }, { + name: "webhook returns error message in body with 200", + settings: `{"url": "http://localhost"}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, }, }, - response: &mockResponse{ - status: 200, - body: "some error message", - error: nil, - }, - expMsgError: errors.New("send notification to Teams: webhook failed validation: some error message"), }, - } + response: &mockResponse{ + status: 200, + body: "some error message", + error: nil, + }, + expMsgError: errors.New("send notification to Teams: webhook failed validation: some error message"), + }} for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 588c0c483a5..1e9bbcfdbcb 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -2196,30 +2196,40 @@ var expNonEmailNotifications = map[string][]string{ }, "teams_recv/teams_test": { `{ - "@context": "http://schema.org/extensions", - "@type": "MessageCard", - "potentialAction": [ + "attachments": [ { - "@context": "http://schema.org", - "@type": "OpenUri", - "name": "View Rule", - "targets": [ - { - "os": "default", - "uri": "http://localhost:3000/alerting/list" - } - ] - } + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": [ + { + "color": "attention", + "size": "large", + "text": "[FIRING:1] TeamsAlert (default)", + "type": "TextBlock", + "weight": "bolder", + "wrap": true + }, { + "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TeamsAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DTeamsAlert\u0026matcher=grafana_folder%3Ddefault\n", + "type": "TextBlock", + "wrap": true + }, { + "actions": [ + { + "title": "View URL", + "type": "Action.OpenUrl", + "url": "http://localhost:3000/alerting/list" + } + ], + "type": "ActionSet" + } + ], + "type": "AdaptiveCard", + "version": "1.4" + }, + "contentType": "application/vnd.microsoft.card.adaptive" + } ], - "sections": [ - { - "text": "**Firing**\n\nValue: [ var='A' labels={} value=1 ]\nLabels:\n - alertname = TeamsAlert\n - grafana_folder = default\nAnnotations:\nSource: http://localhost:3000/alerting/grafana/UID_TeamsAlert/view\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTeamsAlert&matcher=grafana_folder%3Ddefault\n", - "title": "" - } - ], - "summary": "[FIRING:1] TeamsAlert (default)", - "themeColor": "#D63232", - "title": "[FIRING:1] TeamsAlert (default)" + "type": "message" }`, }, "webhook_recv/webhook_test": {