From 8c72d19bccf3bbc6724baccd6724b9d497a31ae2 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Fri, 18 Nov 2022 09:24:12 -0500 Subject: [PATCH] Alerting: Refactor MS teams, Pushover and Webhook notifiers to use encoding/json to parse settings (#56834) * update teams * update sensugo * update pushover * update webhook to use json.Number --- .../ngalert/notifier/channels/pushover.go | 135 +++++++++++------- .../notifier/channels/pushover_test.go | 47 +++++- .../ngalert/notifier/channels/sensugo.go | 130 ++++++++--------- .../ngalert/notifier/channels/sensugo_test.go | 12 +- .../ngalert/notifier/channels/teams.go | 97 ++++++------- .../ngalert/notifier/channels/teams_test.go | 11 +- .../ngalert/notifier/channels/webhook.go | 102 ++++++------- 7 files changed, 312 insertions(+), 222 deletions(-) diff --git a/pkg/services/ngalert/notifier/channels/pushover.go b/pkg/services/ngalert/notifier/channels/pushover.go index 0df40f44a7f..b795f7c6d18 100644 --- a/pkg/services/ngalert/notifier/channels/pushover.go +++ b/pkg/services/ngalert/notifier/channels/pushover.go @@ -3,6 +3,7 @@ package channels import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -42,10 +43,10 @@ type PushoverNotifier struct { type pushoverSettings struct { userKey string apiToken string - alertingPriority int - okPriority int - retry int - expire int + alertingPriority int64 + okPriority int64 + retry int64 + expire int64 device string alertingSound string okSound string @@ -54,40 +55,91 @@ type pushoverSettings struct { message string } +func buildPushoverSettings(fc FactoryConfig) (pushoverSettings, error) { + settings := pushoverSettings{} + rawSettings := struct { + UserKey string `json:"userKey,omitempty" yaml:"userKey,omitempty"` + APIToken string `json:"apiToken,omitempty" yaml:"apiToken,omitempty"` + AlertingPriority json.Number `json:"priority,omitempty" yaml:"priority,omitempty"` + OKPriority json.Number `json:"okPriority,omitempty" yaml:"okPriority,omitempty"` + Retry json.Number `json:"retry,omitempty" yaml:"retry,omitempty"` + Expire json.Number `json:"expire,omitempty" yaml:"expire,omitempty"` + Device string `json:"device,omitempty" yaml:"device,omitempty"` + AlertingSound string `json:"sound,omitempty" yaml:"sound,omitempty"` + OKSound string `json:"okSound,omitempty" yaml:"okSound,omitempty"` + Upload *bool `json:"uploadImage,omitempty" yaml:"uploadImage,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + }{} + + err := fc.Config.unmarshalSettings(&rawSettings) + if err != nil { + return settings, fmt.Errorf("failed to unmarshal settings: %w", err) + } + + settings.userKey = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "userKey", rawSettings.UserKey) + if settings.userKey == "" { + return settings, errors.New("user key not found") + } + settings.apiToken = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "apiToken", rawSettings.APIToken) + if settings.apiToken == "" { + return settings, errors.New("API token not found") + } + if rawSettings.AlertingPriority != "" { + settings.alertingPriority, err = rawSettings.AlertingPriority.Int64() + if err != nil { + return settings, fmt.Errorf("failed to convert alerting priority to integer: %w", err) + } + } + + if rawSettings.OKPriority != "" { + settings.okPriority, err = rawSettings.OKPriority.Int64() + if err != nil { + return settings, fmt.Errorf("failed to convert OK priority to integer: %w", err) + } + } + + settings.retry, _ = rawSettings.Retry.Int64() + settings.expire, _ = rawSettings.Expire.Int64() + + settings.device = rawSettings.Device + settings.alertingSound = rawSettings.AlertingSound + settings.okSound = rawSettings.OKSound + + if rawSettings.Upload == nil || *rawSettings.Upload { + settings.upload = true + } + + settings.message = rawSettings.Message + if settings.message == "" { + settings.message = DefaultMessageEmbed + } + + settings.title = rawSettings.Title + if settings.title == "" { + settings.title = DefaultMessageTitleEmbed + } + + return settings, nil +} + func PushoverFactory(fc FactoryConfig) (NotificationChannel, error) { - pn, err := newPushoverNotifier(fc) + notifier, err := NewPushoverNotifier(fc) if err != nil { return nil, receiverInitError{ Reason: err.Error(), Cfg: *fc.Config, } } - return pn, nil + return notifier, nil } -// newPushoverNotifier is the constructor for the Pushover notifier -func newPushoverNotifier(fc FactoryConfig) (*PushoverNotifier, error) { - decryptFunc := fc.DecryptFunc - userKey := decryptFunc(context.Background(), fc.Config.SecureSettings, "userKey", fc.Config.Settings.Get("userKey").MustString()) - if userKey == "" { - return nil, errors.New("user key not found") - } - apiToken := decryptFunc(context.Background(), fc.Config.SecureSettings, "apiToken", fc.Config.Settings.Get("apiToken").MustString()) - if apiToken == "" { - return nil, errors.New("API token not found") - } - - alertingPriority, err := strconv.Atoi(fc.Config.Settings.Get("priority").MustString("0")) // default Normal +// NewSlackNotifier is the constructor for the Slack notifier +func NewPushoverNotifier(fc FactoryConfig) (*PushoverNotifier, error) { + settings, err := buildPushoverSettings(fc) if err != nil { - return nil, fmt.Errorf("failed to convert alerting priority to integer: %w", err) + return nil, err } - okPriority, err := strconv.Atoi(fc.Config.Settings.Get("okPriority").MustString("0")) // default Normal - if err != nil { - return nil, fmt.Errorf("failed to convert OK priority to integer: %w", err) - } - retry, _ := strconv.Atoi(fc.Config.Settings.Get("retry").MustString()) - expire, _ := strconv.Atoi(fc.Config.Settings.Get("expire").MustString()) - return &PushoverNotifier{ Base: NewBase(&models.AlertNotification{ Uid: fc.Config.UID, @@ -97,24 +149,11 @@ func newPushoverNotifier(fc FactoryConfig) (*PushoverNotifier, error) { Settings: fc.Config.Settings, SecureSettings: fc.Config.SecureSettings, }), - tmpl: fc.Template, - log: log.New("alerting.notifier.pushover"), - images: fc.ImageStore, - ns: fc.NotificationService, - settings: pushoverSettings{ - userKey: userKey, - apiToken: apiToken, - alertingPriority: alertingPriority, - okPriority: okPriority, - retry: retry, - expire: expire, - device: fc.Config.Settings.Get("device").MustString(), - alertingSound: fc.Config.Settings.Get("sound").MustString(), - okSound: fc.Config.Settings.Get("okSound").MustString(), - upload: fc.Config.Settings.Get("uploadImage").MustBool(true), - title: fc.Config.Settings.Get("title").MustString(DefaultMessageTitleEmbed), - message: fc.Config.Settings.Get("message").MustString(DefaultMessageEmbed), - }, + tmpl: fc.Template, + log: log.New("alerting.notifier.pushover"), + images: fc.ImageStore, + ns: fc.NotificationService, + settings: settings, }, nil } @@ -172,16 +211,16 @@ func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Al if status == model.AlertResolved { priority = pn.settings.okPriority } - if err := w.WriteField("priority", strconv.Itoa(priority)); err != nil { + if err := w.WriteField("priority", strconv.FormatInt(priority, 10)); err != nil { return nil, b, fmt.Errorf("failed to write the priority: %w", err) } if priority == 2 { - if err := w.WriteField("retry", strconv.Itoa(pn.settings.retry)); err != nil { + if err := w.WriteField("retry", strconv.FormatInt(pn.settings.retry, 10)); err != nil { return nil, b, fmt.Errorf("failed to write retry: %w", err) } - if err := w.WriteField("expire", strconv.Itoa(pn.settings.expire)); err != nil { + if err := w.WriteField("expire", strconv.FormatInt(pn.settings.expire, 10)); err != nil { return nil, b, fmt.Errorf("failed to write expire: %w", err) } } diff --git a/pkg/services/ngalert/notifier/channels/pushover_test.go b/pkg/services/ngalert/notifier/channels/pushover_test.go index f9c0daa9e9e..d4a80092ae2 100644 --- a/pkg/services/ngalert/notifier/channels/pushover_test.go +++ b/pkg/services/ngalert/notifier/channels/pushover_test.go @@ -140,6 +140,50 @@ func TestPushoverNotifier(t *testing.T) { }, expMsgError: nil, }, + { + name: "Integer fields as integers", + settings: `{ + "userKey": "", + "apiToken": "", + "device": "device", + "priority": 2, + "okpriority": 0, + "retry": 30, + "expire": 86400, + "sound": "echo", + "oksound": "magic", + "message": "{{ len .Alerts.Firing }} alerts are firing, {{ len .Alerts.Resolved }} are resolved" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__alertImageToken__": "test-image-1"}, + }, + }, { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, + Annotations: model.LabelSet{"ann1": "annv2", "__alertImageToken__": "test-image-2"}, + }, + }, + }, + expMsg: map[string]string{ + "user": "", + "token": "", + "priority": "2", + "sound": "echo", + "title": "[FIRING:2] ", + "url": "http://localhost/alerting/list", + "url_title": "Show alert rule", + "message": "2 alerts are firing, 0 are resolved", + "attachment": "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\b\x04\x00\x00\x00\xb5\x1c\f\x02\x00\x00\x00\vIDATx\xdacd`\x00\x00\x00\x06\x00\x020\x81\xd0/\x00\x00\x00\x00IEND\xaeB`\x82", + "html": "1", + "retry": "30", + "expire": "86400", + "device": "device", + }, + expMsgError: nil, + }, { name: "Missing user key", settings: `{ @@ -186,7 +230,8 @@ func TestPushoverNotifier(t *testing.T) { DecryptFunc: decryptFn, Template: tmpl, } - pn, err := newPushoverNotifier(fc) + + pn, err := NewPushoverNotifier(fc) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) diff --git a/pkg/services/ngalert/notifier/channels/sensugo.go b/pkg/services/ngalert/notifier/channels/sensugo.go index 66de07cf0a4..ead78de5a36 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo.go +++ b/pkg/services/ngalert/notifier/channels/sensugo.go @@ -19,86 +19,74 @@ import ( type SensuGoNotifier struct { *Base - log log.Logger - images ImageStore - ns notifications.WebhookSender - tmpl *template.Template - - URL string - Entity string - Check string - Namespace string - Handler string - APIKey string - Message string + log log.Logger + images ImageStore + ns notifications.WebhookSender + tmpl *template.Template + settings sensuGoSettings } -type SensuGoConfig struct { - *NotificationChannelConfig - URL string - Entity string - Check string - Namespace string - Handler string - APIKey string - Message string +type sensuGoSettings struct { + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Entity string `json:"entity,omitempty" yaml:"entity,omitempty"` + Check string `json:"check,omitempty" yaml:"check,omitempty"` + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + Handler string `json:"handler,omitempty" yaml:"handler,omitempty"` + APIKey string `json:"apikey,omitempty" yaml:"apikey,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func buildSensuGoConfig(fc FactoryConfig) (sensuGoSettings, error) { + settings := sensuGoSettings{} + err := fc.Config.unmarshalSettings(&settings) + if err != nil { + return settings, fmt.Errorf("failed to unmarshal settings: %w", err) + } + if settings.URL == "" { + return settings, errors.New("could not find URL property in settings") + } + settings.APIKey = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "apikey", settings.APIKey) + if settings.APIKey == "" { + return settings, errors.New("could not find the API key property in settings") + } + if settings.Message == "" { + settings.Message = DefaultMessageEmbed + } + return settings, nil } func SensuGoFactory(fc FactoryConfig) (NotificationChannel, error) { - cfg, err := NewSensuGoConfig(fc.Config, fc.DecryptFunc) + notifier, err := NewSensuGoNotifier(fc) if err != nil { return nil, receiverInitError{ Reason: err.Error(), Cfg: *fc.Config, } } - return NewSensuGoNotifier(cfg, fc.ImageStore, fc.NotificationService, fc.Template), nil -} - -func NewSensuGoConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*SensuGoConfig, error) { - url := config.Settings.Get("url").MustString() - if url == "" { - return nil, errors.New("could not find URL property in settings") - } - apikey := decryptFunc(context.Background(), config.SecureSettings, "apikey", config.Settings.Get("apikey").MustString()) - if apikey == "" { - return nil, errors.New("could not find the API key property in settings") - } - return &SensuGoConfig{ - NotificationChannelConfig: config, - URL: url, - Entity: config.Settings.Get("entity").MustString(), - Check: config.Settings.Get("check").MustString(), - Namespace: config.Settings.Get("namespace").MustString(), - Handler: config.Settings.Get("handler").MustString(), - APIKey: apikey, - Message: config.Settings.Get("message").MustString(DefaultMessageEmbed), - }, nil + return notifier, nil } // NewSensuGoNotifier is the constructor for the SensuGo notifier -func NewSensuGoNotifier(config *SensuGoConfig, images ImageStore, ns notifications.WebhookSender, t *template.Template) *SensuGoNotifier { +func NewSensuGoNotifier(fc FactoryConfig) (*SensuGoNotifier, error) { + settings, err := buildSensuGoConfig(fc) + if err != nil { + return nil, err + } return &SensuGoNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: config.UID, - Name: config.Name, - Type: config.Type, - DisableResolveMessage: config.DisableResolveMessage, - Settings: config.Settings, - SecureSettings: config.SecureSettings, + Uid: fc.Config.UID, + Name: fc.Config.Name, + Type: fc.Config.Type, + DisableResolveMessage: fc.Config.DisableResolveMessage, + Settings: fc.Config.Settings, + SecureSettings: fc.Config.SecureSettings, }), - URL: config.URL, - Entity: config.Entity, - Check: config.Check, - Namespace: config.Namespace, - Handler: config.Handler, - APIKey: config.APIKey, - Message: config.Message, - log: log.New("alerting.notifier.sensugo"), - images: images, - ns: ns, - tmpl: t, - } + log: log.New("alerting.notifier.sensugo"), + images: fc.ImageStore, + ns: fc.NotificationService, + tmpl: fc.Template, + settings: settings, + }, nil } // Notify sends an alert notification to Sensu Go @@ -110,12 +98,12 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool // Sensu Go alerts require an entity and a check. We set it to the user-specified // value (optional), else we fallback and use the grafana rule anme and ruleID. - entity := tmpl(sn.Entity) + entity := tmpl(sn.settings.Entity) if entity == "" { entity = "default" } - check := tmpl(sn.Check) + check := tmpl(sn.settings.Check) if check == "" { check = "default" } @@ -127,14 +115,14 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool status = 2 } - namespace := tmpl(sn.Namespace) + namespace := tmpl(sn.settings.Namespace) if namespace == "" { namespace = "default" } var handlers []string - if sn.Handler != "" { - handlers = []string{tmpl(sn.Handler)} + if sn.settings.Handler != "" { + handlers = []string{tmpl(sn.settings.Handler)} } labels := make(map[string]string) @@ -166,7 +154,7 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool "name": check, "labels": labels, }, - "output": tmpl(sn.Message), + "output": tmpl(sn.settings.Message), "issued": timeNow().Unix(), "interval": 86400, "status": status, @@ -185,12 +173,12 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool } cmd := &models.SendWebhookSync{ - Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.URL, "/"), namespace), + Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.settings.URL, "/"), namespace), Body: string(body), HttpMethod: "POST", HttpHeader: map[string]string{ "Content-Type": "application/json", - "Authorization": fmt.Sprintf("Key %s", sn.APIKey), + "Authorization": fmt.Sprintf("Key %s", sn.settings.APIKey), }, } if err := sn.ns.SendWebhookSync(ctx, cmd); err != nil { diff --git a/pkg/services/ngalert/notifier/channels/sensugo_test.go b/pkg/services/ngalert/notifier/channels/sensugo_test.go index ea0a8a9a863..74bbdf0c5fa 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo_test.go +++ b/pkg/services/ngalert/notifier/channels/sensugo_test.go @@ -151,7 +151,16 @@ func TestSensuGoNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - cfg, err := NewSensuGoConfig(m, decryptFn) + + fc := FactoryConfig{ + Config: m, + ImageStore: images, + NotificationService: webhookSender, + Template: tmpl, + DecryptFunc: decryptFn, + } + + sn, err := NewSensuGoNotifier(fc) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -161,7 +170,6 @@ func TestSensuGoNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) - sn := NewSensuGoNotifier(cfg, images, webhookSender, tmpl) ok, err := sn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index d924ca98f82..690b9f7919a 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -224,70 +224,71 @@ func (i AdaptiveCardOpenURLActionItem) MarshalJSON() ([]byte, error) { }) } -type TeamsConfig struct { - *NotificationChannelConfig - URL string - Message string - Title string - SectionTitle string +type teamsSettings struct { + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + SectionTitle string `json:"sectiontitle,omitempty" yaml:"sectiontitle,omitempty"` } -func NewTeamsConfig(config *NotificationChannelConfig) (*TeamsConfig, error) { - URL := config.Settings.Get("url").MustString() - if URL == "" { - return nil, errors.New("could not find url property in settings") +func buildTeamsSettings(fc FactoryConfig) (teamsSettings, error) { + settings := teamsSettings{} + err := fc.Config.unmarshalSettings(&settings) + if err != nil { + return settings, fmt.Errorf("failed to unmarshal settings: %w", err) } - return &TeamsConfig{ - NotificationChannelConfig: config, - URL: URL, - Message: config.Settings.Get("message").MustString(`{{ template "teams.default.message" .}}`), - Title: config.Settings.Get("title").MustString(DefaultMessageTitleEmbed), - SectionTitle: config.Settings.Get("sectiontitle").MustString(""), - }, nil + if settings.URL == "" { + return settings, errors.New("could not find url property in settings") + } + if settings.Message == "" { + settings.Message = `{{ template "teams.default.message" .}}` + } + if settings.Title == "" { + settings.Title = DefaultMessageTitleEmbed + } + return settings, nil } type TeamsNotifier struct { *Base - URL string - Message string - Title string - SectionTitle string - tmpl *template.Template - log log.Logger - ns notifications.WebhookSender - images ImageStore + tmpl *template.Template + log log.Logger + ns notifications.WebhookSender + images ImageStore + settings teamsSettings } // NewTeamsNotifier is the constructor for Teams notifier. -func NewTeamsNotifier(config *TeamsConfig, ns notifications.WebhookSender, images ImageStore, t *template.Template) *TeamsNotifier { +func NewTeamsNotifier(fc FactoryConfig) (*TeamsNotifier, error) { + settings, err := buildTeamsSettings(fc) + if err != nil { + return nil, err + } return &TeamsNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: config.UID, - Name: config.Name, - Type: config.Type, - DisableResolveMessage: config.DisableResolveMessage, - Settings: config.Settings, + Uid: fc.Config.UID, + Name: fc.Config.Name, + Type: fc.Config.Type, + DisableResolveMessage: fc.Config.DisableResolveMessage, + Settings: fc.Config.Settings, }), - URL: config.URL, - Message: config.Message, - Title: config.Title, - SectionTitle: config.SectionTitle, - log: log.New("alerting.notifier.teams"), - ns: ns, - images: images, - tmpl: t, - } + log: log.New("alerting.notifier.teams"), + ns: fc.NotificationService, + images: fc.ImageStore, + tmpl: fc.Template, + settings: settings, + }, nil } func TeamsFactory(fc FactoryConfig) (NotificationChannel, error) { - cfg, err := NewTeamsConfig(fc.Config) + notifier, err := NewTeamsNotifier(fc) if err != nil { return nil, receiverInitError{ Reason: err.Error(), Cfg: *fc.Config, } } - return NewTeamsNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil + return notifier, nil } func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { @@ -297,13 +298,13 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, card := NewAdaptiveCard() card.AppendItem(AdaptiveCardTextBlockItem{ Color: getTeamsTextColor(types.Alerts(as...)), - Text: tmpl(tn.Title), + Text: tmpl(tn.settings.Title), Size: TextSizeLarge, Weight: TextWeightBolder, Wrap: true, }) card.AppendItem(AdaptiveCardTextBlockItem{ - Text: tmpl(tn.Message), + Text: tmpl(tn.settings.Message), Wrap: true, }) @@ -335,7 +336,7 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, }) msg := NewAdaptiveCardsMessage(card) - msg.Summary = tmpl(tn.Title) + msg.Summary = tmpl(tn.settings.Title) // This check for tmplErr must happen before templating the URL if tmplErr != nil { @@ -343,10 +344,10 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, tmplErr = nil } - u := tmpl(tn.URL) + u := tmpl(tn.settings.URL) if tmplErr != nil { - tn.log.Warn("failed to template Teams URL", "error", tmplErr.Error(), "fallback", tn.URL) - u = tn.URL + tn.log.Warn("failed to template Teams URL", "error", tmplErr.Error(), "fallback", tn.settings.URL) + u = tn.settings.URL } b, err := json.Marshal(msg) diff --git a/pkg/services/ngalert/notifier/channels/teams_test.go b/pkg/services/ngalert/notifier/channels/teams_test.go index 3d0c28a10a7..b704fbb70f3 100644 --- a/pkg/services/ngalert/notifier/channels/teams_test.go +++ b/pkg/services/ngalert/notifier/channels/teams_test.go @@ -289,7 +289,14 @@ func TestTeamsNotifier(t *testing.T) { clientStub := newMockClient(c.response) notifications.SetWebhookClient(clientStub) - cfg, err := NewTeamsConfig(m) + fc := FactoryConfig{ + Config: m, + ImageStore: &UnavailableImageStore{}, + NotificationService: webhookSender, + Template: tmpl, + } + + pn, err := NewTeamsNotifier(fc) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -299,7 +306,7 @@ func TestTeamsNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) - pn := NewTeamsNotifier(cfg, webhookSender, &UnavailableImageStore{}, tmpl) + ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go index d5aaff281a1..e1e7cc0d0d4 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -23,55 +23,76 @@ import ( // alert notifications as webhooks. type WebhookNotifier struct { *Base - log log.Logger - ns notifications.WebhookSender - images ImageStore - tmpl *template.Template - orgID int64 - maxAlerts int - settings webhookSettings + log log.Logger + ns notifications.WebhookSender + images ImageStore + tmpl *template.Template + orgID int64 + settings webhookSettings } type webhookSettings struct { - URL string `json:"url,omitempty" yaml:"url,omitempty"` - HTTPMethod string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty"` - MaxAlerts interface{} `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty"` - + URL string + HTTPMethod string + MaxAlerts int // Authorization Header. - AuthorizationScheme string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty"` - AuthorizationCredentials string `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty"` + AuthorizationScheme string + AuthorizationCredentials string // HTTP Basic Authentication. - User string `json:"username,omitempty" yaml:"username,omitempty"` - Password string `json:"password,omitempty" yaml:"password,omitempty"` + User string + Password string - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Message string `json:"message,omitempty" yaml:"message,omitempty"` + Title string + Message string } func buildWebhookSettings(factoryConfig FactoryConfig) (webhookSettings, error) { settings := webhookSettings{} - err := factoryConfig.Config.unmarshalSettings(&settings) + rawSettings := struct { + URL string `json:"url,omitempty" yaml:"url,omitempty"` + HTTPMethod string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty"` + MaxAlerts json.Number `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty"` + AuthorizationScheme string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty"` + AuthorizationCredentials string `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty"` + User string `json:"username,omitempty" yaml:"username,omitempty"` + Password string `json:"password,omitempty" yaml:"password,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + }{} + + err := factoryConfig.Config.unmarshalSettings(&rawSettings) if err != nil { return settings, fmt.Errorf("failed to unmarshal settings: %w", err) } - if settings.URL == "" { + if rawSettings.URL == "" { return settings, errors.New("required field 'url' is not specified") } - if settings.HTTPMethod == "" { - settings.HTTPMethod = http.MethodPost + settings.URL = rawSettings.URL + + if rawSettings.HTTPMethod == "" { + rawSettings.HTTPMethod = http.MethodPost } - settings.User = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "username", settings.User) - settings.Password = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "password", settings.Password) - settings.AuthorizationCredentials = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "authorization_scheme", settings.AuthorizationCredentials) + settings.HTTPMethod = rawSettings.HTTPMethod + + if rawSettings.MaxAlerts != "" { + settings.MaxAlerts, _ = strconv.Atoi(rawSettings.MaxAlerts.String()) + } + + settings.User = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "username", rawSettings.User) + settings.Password = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "password", rawSettings.Password) + settings.AuthorizationCredentials = factoryConfig.DecryptFunc(context.Background(), factoryConfig.Config.SecureSettings, "authorization_scheme", rawSettings.AuthorizationCredentials) + if settings.AuthorizationCredentials != "" && settings.AuthorizationScheme == "" { settings.AuthorizationScheme = "Bearer" } if settings.User != "" && settings.Password != "" && settings.AuthorizationScheme != "" && settings.AuthorizationCredentials != "" { return settings, errors.New("both HTTP Basic Authentication and Authorization Header are set, only 1 is permitted") } + settings.Title = rawSettings.Title if settings.Title == "" { settings.Title = DefaultMessageTitleEmbed } + settings.Message = rawSettings.Message if settings.Message == "" { settings.Message = DefaultMessageEmbed } @@ -96,24 +117,6 @@ func buildWebhookNotifier(factoryConfig FactoryConfig) (*WebhookNotifier, error) if err != nil { return nil, err } - - logger := log.New("alerting.notifier.webhook") - maxAlerts := 0 - if settings.MaxAlerts != nil { - switch value := settings.MaxAlerts.(type) { - case int: - maxAlerts = value - case string: - maxAlerts, err = strconv.Atoi(value) - if err != nil { - logger.Warn("failed to convert setting maxAlerts to integer. Using default", "error", err, "original", value) - maxAlerts = 0 - } - default: - logger.Warn("unexpected type of setting maxAlerts. Expected integer. Using default", "type", fmt.Sprintf("%T", settings.MaxAlerts)) - } - } - return &WebhookNotifier{ Base: NewBase(&models.AlertNotification{ Uid: factoryConfig.Config.UID, @@ -122,13 +125,12 @@ func buildWebhookNotifier(factoryConfig FactoryConfig) (*WebhookNotifier, error) DisableResolveMessage: factoryConfig.Config.DisableResolveMessage, Settings: factoryConfig.Config.Settings, }), - orgID: factoryConfig.Config.OrgID, - log: logger, - ns: factoryConfig.NotificationService, - images: factoryConfig.ImageStore, - tmpl: factoryConfig.Template, - maxAlerts: maxAlerts, - settings: settings, + orgID: factoryConfig.Config.OrgID, + log: log.New("alerting.notifier.webhook"), + ns: factoryConfig.NotificationService, + images: factoryConfig.ImageStore, + tmpl: factoryConfig.Template, + settings: settings, }, nil } @@ -153,7 +155,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool return false, err } - as, numTruncated := truncateAlerts(wn.maxAlerts, as) + as, numTruncated := truncateAlerts(wn.settings.MaxAlerts, as) var tmplErr error tmpl, data := TmplText(ctx, wn.tmpl, as, wn.log, &tmplErr)