diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 2ebe423be3b..2a5c1374186 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -451,11 +451,6 @@ func (am *Alertmanager) buildIntegrationsMap(receivers []*apimodels.PostableApiR return integrationsMap, nil } -type NotificationChannel interface { - notify.Notifier - notify.ResolvedSender -} - // buildReceiverIntegrations builds a list of integration notifiers off of a receiver config. func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableApiReceiver, tmpl *template.Template) ([]notify.Integration, error) { var integrations []notify.Integration @@ -469,7 +464,7 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp return integrations, nil } -func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaReceiver, tmpl *template.Template) (NotificationChannel, error) { +func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaReceiver, tmpl *template.Template) (channels.NotificationChannel, error) { // secure settings are already encrypted at this point secureSettings := make(map[string][]byte, len(r.SecureSettings)) @@ -494,60 +489,28 @@ func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaRec Settings: r.Settings, SecureSettings: secureSettings, } - n NotificationChannel - err error ) - switch r.Type { - case "email": - n, err = channels.NewEmailNotifier(cfg, am.NotificationService, tmpl) // Email notifier already has a default template. - case "pagerduty": - n, err = channels.NewPagerdutyNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "pushover": - n, err = channels.NewPushoverNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "slack": - n, err = channels.NewSlackNotifier(cfg, tmpl, am.decryptFn) - case "telegram": - n, err = channels.NewTelegramNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "victorops": - n, err = channels.NewVictoropsNotifier(cfg, am.NotificationService, tmpl) - case "teams": - n, err = channels.NewTeamsNotifier(cfg, am.NotificationService, tmpl) - case "dingding": - n, err = channels.NewDingDingNotifier(cfg, am.NotificationService, tmpl) - case "kafka": - n, err = channels.NewKafkaNotifier(cfg, am.NotificationService, tmpl) - case "webhook": - n, err = channels.NewWebHookNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "wecom": - n, err = channels.NewWeComNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "sensugo": - n, err = channels.NewSensuGoNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "discord": - n, err = channels.NewDiscordNotifier(cfg, am.NotificationService, tmpl) - case "googlechat": - n, err = channels.NewGoogleChatNotifier(cfg, am.NotificationService, tmpl) - case "LINE": - n, err = channels.NewLineNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "threema": - n, err = channels.NewThreemaNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "opsgenie": - n, err = channels.NewOpsgenieNotifier(cfg, am.NotificationService, tmpl, am.decryptFn) - case "prometheus-alertmanager": - n, err = channels.NewAlertmanagerNotifier(cfg, tmpl, am.decryptFn) - default: + factoryConfig, err := channels.NewFactoryConfig(cfg, am.NotificationService, am.decryptFn, tmpl) + if err != nil { return nil, InvalidReceiverError{ Receiver: r, + Err: err, + } + } + receiverFactory, exists := channels.Factory(r.Type) + if !exists { + return nil, InvalidReceiverError{ + Receiver: r, Err: fmt.Errorf("notifier %s is not supported", r.Type), } } - + n, err := receiverFactory(factoryConfig) if err != nil { return nil, InvalidReceiverError{ Receiver: r, Err: err, } } - return n, nil } diff --git a/pkg/services/ngalert/notifier/channels/alertmanager.go b/pkg/services/ngalert/notifier/channels/alertmanager.go index 6f9abc0d242..f526c150473 100644 --- a/pkg/services/ngalert/notifier/channels/alertmanager.go +++ b/pkg/services/ngalert/notifier/channels/alertmanager.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "fmt" "net/url" "strings" @@ -17,49 +18,64 @@ import ( // the given key. If the key is not present, then it returns the fallback value. type GetDecryptedValueFn func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string -// NewAlertmanagerNotifier returns a new Alertmanager notifier. -func NewAlertmanagerNotifier(model *NotificationChannelConfig, _ *template.Template, fn GetDecryptedValueFn) (*AlertmanagerNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } - urlStr := model.Settings.Get("url").MustString() - if urlStr == "" { - return nil, receiverInitError{Reason: "could not find url property in settings", Cfg: *model} - } +type AlertmanagerConfig struct { + *NotificationChannelConfig + URLs []*url.URL + BasicAuthUser string + BasicAuthPassword string +} +func NewAlertmanagerConfig(config *NotificationChannelConfig, fn GetDecryptedValueFn) (*AlertmanagerConfig, error) { + urlStr := config.Settings.Get("url").MustString() + if urlStr == "" { + return nil, errors.New("could not find url property in settings") + } var urls []*url.URL for _, uS := range strings.Split(urlStr, ",") { uS = strings.TrimSpace(uS) if uS == "" { continue } - uS = strings.TrimSuffix(uS, "/") + "/api/v1/alerts" - u, err := url.Parse(uS) + url, err := url.Parse(uS) if err != nil { - return nil, receiverInitError{Reason: "invalid url property in settings", Cfg: *model, Err: err} + return nil, fmt.Errorf("invalid url property in settings: %w", err) } - - urls = append(urls, u) + urls = append(urls, url) } - basicAuthUser := model.Settings.Get("basicAuthUser").MustString() - basicAuthPassword := fn(context.Background(), model.SecureSettings, "basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString()) + return &AlertmanagerConfig{ + NotificationChannelConfig: config, + URLs: urls, + BasicAuthUser: config.Settings.Get("basicAuthUser").MustString(), + BasicAuthPassword: fn(context.Background(), config.SecureSettings, "basicAuthPassword", config.Settings.Get("basicAuthPassword").MustString()), + }, nil +} +func AlertmanagerFactory(fc FactoryConfig) (NotificationChannel, error) { + config, err := NewAlertmanagerConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewAlertmanagerNotifier(config, nil, fc.DecryptFunc), nil +} + +// NewAlertmanagerNotifier returns a new Alertmanager notifier. +func NewAlertmanagerNotifier(config *AlertmanagerConfig, _ *template.Template, fn GetDecryptedValueFn) *AlertmanagerNotifier { return &AlertmanagerNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - urls: urls, - basicAuthUser: basicAuthUser, - basicAuthPassword: basicAuthPassword, + urls: config.URLs, + basicAuthUser: config.BasicAuthUser, + basicAuthPassword: config.BasicAuthPassword, logger: log.New("alerting.notifier.prometheus-alertmanager"), - }, nil + } } // AlertmanagerNotifier sends alert notifications to the alert manager diff --git a/pkg/services/ngalert/notifier/channels/alertmanager_test.go b/pkg/services/ngalert/notifier/channels/alertmanager_test.go index b548edf4b59..fb77c415e11 100644 --- a/pkg/services/ngalert/notifier/channels/alertmanager_test.go +++ b/pkg/services/ngalert/notifier/channels/alertmanager_test.go @@ -35,13 +35,13 @@ func TestNewAlertmanagerNotifier(t *testing.T) { { name: "Error in initing: missing URL", settings: `{}`, - expectedInitError: `failed to validate receiver of type "alertmanager": could not find url property in settings`, + expectedInitError: `could not find url property in settings`, }, { name: "Error in initing: invalid URL", settings: `{ "url": "://alertmanager.com" }`, - expectedInitError: `failed to validate receiver "Alertmanager" of type "alertmanager": invalid url property in settings: parse "://alertmanager.com/api/v1/alerts": missing protocol scheme`, + expectedInitError: `invalid url property in settings: parse "://alertmanager.com/api/v1/alerts": missing protocol scheme`, receiverName: "Alertmanager", }, } @@ -53,19 +53,20 @@ func TestNewAlertmanagerNotifier(t *testing.T) { m := &NotificationChannelConfig{ Name: c.receiverName, - Type: "alertmanager", + Type: "prometheus-alertmanager", Settings: settingsJSON, SecureSettings: secureSettings, } secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - sn, err := NewAlertmanagerNotifier(m, tmpl, decryptFn) + cfg, err := NewAlertmanagerConfig(m, decryptFn) if c.expectedInitError != "" { require.Equal(t, c.expectedInitError, err.Error()) return } require.NoError(t, err) + sn := NewAlertmanagerNotifier(cfg, tmpl, decryptFn) require.NotNil(t, sn) }) } @@ -136,16 +137,16 @@ func TestAlertmanagerNotifier_Notify(t *testing.T) { m := &NotificationChannelConfig{ Name: c.receiverName, - Type: "alertmanager", + Type: "prometheus-alertmanager", Settings: settingsJSON, SecureSettings: secureSettings, } secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - sn, err := NewAlertmanagerNotifier(m, tmpl, decryptFn) + cfg, err := NewAlertmanagerConfig(m, decryptFn) require.NoError(t, err) - + sn := NewAlertmanagerNotifier(cfg, tmpl, decryptFn) var body []byte origSendHTTPRequest := sendHTTPRequest t.Cleanup(func() { diff --git a/pkg/services/ngalert/notifier/channels/dingding.go b/pkg/services/ngalert/notifier/channels/dingding.go index bca261e756c..9e92a4c6d83 100644 --- a/pkg/services/ngalert/notifier/channels/dingding.go +++ b/pkg/services/ngalert/notifier/channels/dingding.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "fmt" "net/url" @@ -16,34 +17,53 @@ import ( const defaultDingdingMsgType = "link" -// NewDingDingNotifier is the constructor for the Dingding notifier -func NewDingDingNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*DingDingNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } +type DingDingConfig struct { + *NotificationChannelConfig + MsgType string + Message string + URL string +} - url := model.Settings.Get("url").MustString() +func NewDingDingConfig(config *NotificationChannelConfig) (*DingDingConfig, error) { + url := config.Settings.Get("url").MustString() if url == "" { - return nil, receiverInitError{Reason: "could not find url property in settings", Cfg: *model} + return nil, errors.New("could not find url property in settings") } + return &DingDingConfig{ + NotificationChannelConfig: config, + MsgType: config.Settings.Get("msgType").MustString(defaultDingdingMsgType), + Message: config.Settings.Get("message").MustString(`{{ template "default.message" .}}`), + URL: config.Settings.Get("url").MustString(), + }, nil +} +func DingDingFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewDingDingConfig(fc.Config) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewDingDingNotifier(cfg, fc.NotificationService, fc.Template), nil +} - msgType := model.Settings.Get("msgType").MustString(defaultDingdingMsgType) - +// NewDingDingNotifier is the constructor for the Dingding notifier +func NewDingDingNotifier(config *DingDingConfig, ns notifications.WebhookSender, t *template.Template) *DingDingNotifier { return &DingDingNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - MsgType: msgType, - URL: url, - Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`), + MsgType: config.MsgType, + Message: config.Message, + URL: config.URL, log: log.New("alerting.notifier.dingding"), tmpl: t, ns: ns, - }, nil + } } // DingDingNotifier is responsible for sending alert notifications to ding ding. diff --git a/pkg/services/ngalert/notifier/channels/dingding_test.go b/pkg/services/ngalert/notifier/channels/dingding_test.go index 64df838fdcf..a4b9fd8a286 100644 --- a/pkg/services/ngalert/notifier/channels/dingding_test.go +++ b/pkg/services/ngalert/notifier/channels/dingding_test.go @@ -82,7 +82,7 @@ func TestDingdingNotifier(t *testing.T) { }, { name: "Error in initing", settings: `{}`, - expInitError: `failed to validate receiver "dingding_testing" of type "dingding": could not find url property in settings`, + expInitError: `could not find url property in settings`, }, } @@ -98,7 +98,7 @@ func TestDingdingNotifier(t *testing.T) { } webhookSender := mockNotificationService() - pn, err := NewDingDingNotifier(m, webhookSender, tmpl) + cfg, err := NewDingDingConfig(m) if c.expInitError != "" { require.Equal(t, c.expInitError, err.Error()) return @@ -107,6 +107,7 @@ func TestDingdingNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewDingDingNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/discord.go b/pkg/services/ngalert/notifier/channels/discord.go index 3937a1cfe4e..89b8e463dd6 100644 --- a/pkg/services/ngalert/notifier/channels/discord.go +++ b/pkg/services/ngalert/notifier/channels/discord.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "strconv" "strings" @@ -27,39 +28,57 @@ type DiscordNotifier struct { UseDiscordUsername bool } -func NewDiscordNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*DiscordNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } +type DiscordConfig struct { + *NotificationChannelConfig + Content string + AvatarURL string + WebhookURL string + UseDiscordUsername bool +} - avatarURL := model.Settings.Get("avatar_url").MustString() - - discordURL := model.Settings.Get("url").MustString() +func NewDiscordConfig(config *NotificationChannelConfig) (*DiscordConfig, error) { + discordURL := config.Settings.Get("url").MustString() if discordURL == "" { - return nil, receiverInitError{Reason: "could not find webhook url property in settings", Cfg: *model} + return nil, errors.New("could not find webhook url property in settings") } + return &DiscordConfig{ + NotificationChannelConfig: config, + Content: config.Settings.Get("message").MustString(`{{ template "default.message" . }}`), + AvatarURL: config.Settings.Get("avatar_url").MustString(), + WebhookURL: discordURL, + UseDiscordUsername: config.Settings.Get("use_discord_username").MustBool(false), + }, nil +} - useDiscordUsername := model.Settings.Get("use_discord_username").MustBool(false) - - content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`) +func DiscrodFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewDiscordConfig(fc.Config) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewDiscordNotifier(cfg, fc.NotificationService, fc.Template), nil +} +func NewDiscordNotifier(config *DiscordConfig, ns notifications.WebhookSender, t *template.Template) *DiscordNotifier { return &DiscordNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, - SecureSettings: model.SecureSettings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, + SecureSettings: config.SecureSettings, }), - Content: content, - AvatarURL: avatarURL, - WebhookURL: discordURL, + Content: config.Content, + AvatarURL: config.AvatarURL, + WebhookURL: config.WebhookURL, log: log.New("alerting.notifier.discord"), ns: ns, tmpl: t, - UseDiscordUsername: useDiscordUsername, - }, nil + UseDiscordUsername: config.UseDiscordUsername, + } } func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { diff --git a/pkg/services/ngalert/notifier/channels/discord_test.go b/pkg/services/ngalert/notifier/channels/discord_test.go index 95454a1f79e..e94cee2cbe2 100644 --- a/pkg/services/ngalert/notifier/channels/discord_test.go +++ b/pkg/services/ngalert/notifier/channels/discord_test.go @@ -98,7 +98,7 @@ func TestDiscordNotifier(t *testing.T) { { name: "Error in initialization", settings: `{}`, - expInitError: `failed to validate receiver "discord_testing" of type "discord": could not find webhook url property in settings`, + expInitError: `could not find webhook url property in settings`, }, { name: "Invalid template returns error", @@ -151,7 +151,7 @@ func TestDiscordNotifier(t *testing.T) { } webhookSender := mockNotificationService() - dn, err := NewDiscordNotifier(m, webhookSender, tmpl) + cfg, err := NewDiscordConfig(m) if c.expInitError != "" { require.Equal(t, c.expInitError, err.Error()) return @@ -160,6 +160,7 @@ func TestDiscordNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + dn := NewDiscordNotifier(cfg, webhookSender, tmpl) ok, err := dn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index 47d23d67243..c87b88e2049 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -2,6 +2,7 @@ package channels import ( "context" + "errors" "net/url" "path" @@ -26,38 +27,57 @@ type EmailNotifier struct { tmpl *template.Template } -// NewEmailNotifier is the constructor function -// for the EmailNotifier. -func NewEmailNotifier(model *NotificationChannelConfig, ns notifications.EmailSender, t *template.Template) (*EmailNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} +type EmailConfig struct { + *NotificationChannelConfig + SingleEmail bool + Addresses []string + Message string +} + +func EmailFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewEmailConfig(fc.Config) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } } + return NewEmailNotifier(cfg, fc.NotificationService, fc.Template), nil +} - addressesString := model.Settings.Get("addresses").MustString() - singleEmail := model.Settings.Get("singleEmail").MustBool(false) - +func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) { + addressesString := config.Settings.Get("addresses").MustString() if addressesString == "" { - return nil, receiverInitError{Reason: "could not find addresses in settings", Cfg: *model} + return nil, errors.New("could not find addresses in settings") } - // split addresses with a few different ways addresses := util.SplitEmails(addressesString) + return &EmailConfig{ + NotificationChannelConfig: config, + SingleEmail: config.Settings.Get("singleEmail").MustBool(false), + Message: config.Settings.Get("message").MustString(), + Addresses: addresses, + }, nil +} +// NewEmailNotifier is the constructor function +// for the EmailNotifier. +func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, t *template.Template) *EmailNotifier { return &EmailNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - Addresses: addresses, - SingleEmail: singleEmail, - Message: model.Settings.Get("message").MustString(), + Addresses: config.Addresses, + SingleEmail: config.SingleEmail, + Message: config.Message, log: log.New("alerting.notifier.email"), ns: ns, tmpl: t, - }, nil + } } // Notify sends the alert notification. diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index 9cd438cfd66..90b70445740 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -33,7 +33,7 @@ func TestEmailNotifier(t *testing.T) { Settings: settingsJSON, } - _, err := NewEmailNotifier(model, nil, tmpl) + _, err := NewEmailConfig(model) require.Error(t, err) }) @@ -46,14 +46,13 @@ func TestEmailNotifier(t *testing.T) { require.NoError(t, err) emailSender := mockNotificationService() - - emailNotifier, err := NewEmailNotifier(&NotificationChannelConfig{ + cfg, err := NewEmailConfig(&NotificationChannelConfig{ Name: "ops", Type: "email", Settings: settingsJSON, - }, emailSender, tmpl) - + }) require.NoError(t, err) + emailNotifier := NewEmailNotifier(cfg, emailSender, tmpl) alerts := []*types.Alert{ { @@ -284,13 +283,13 @@ func createSut(t *testing.T, messageTmpl string, emailTmpl *template.Template, n settingsJSON.Set("message", messageTmpl) } require.NoError(t, err) - - emailNotifier, err := NewEmailNotifier(&NotificationChannelConfig{ + cfg, err := NewEmailConfig(&NotificationChannelConfig{ Name: "ops", Type: "email", Settings: settingsJSON, - }, ns, emailTmpl) + }) require.NoError(t, err) + emailNotifier := NewEmailNotifier(cfg, ns, emailTmpl) return emailNotifier } diff --git a/pkg/services/ngalert/notifier/channels/factory.go b/pkg/services/ngalert/notifier/channels/factory.go new file mode 100644 index 00000000000..ed5d9804724 --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/factory.go @@ -0,0 +1,61 @@ +package channels + +import ( + "errors" + "strings" + + "github.com/grafana/grafana/pkg/services/notifications" + "github.com/prometheus/alertmanager/template" +) + +type FactoryConfig struct { + Config *NotificationChannelConfig + NotificationService notifications.Service + DecryptFunc GetDecryptedValueFn + Template *template.Template +} + +func NewFactoryConfig(config *NotificationChannelConfig, notificationService notifications.Service, + decryptFunc GetDecryptedValueFn, template *template.Template) (FactoryConfig, error) { + if config.Settings == nil { + return FactoryConfig{}, errors.New("no settings supplied") + } + // not all receivers do need secure settings, we still might interact with + // them, so we make sure they are never nil + if config.SecureSettings == nil { + config.SecureSettings = map[string][]byte{} + } + return FactoryConfig{ + Config: config, + NotificationService: notificationService, + DecryptFunc: decryptFunc, + Template: template, + }, nil +} + +var receiverFactories = map[string]func(FactoryConfig) (NotificationChannel, error){ + "prometheus-alertmanager": AlertmanagerFactory, + "dingding": DingDingFactory, + "discord": DiscrodFactory, + "email": EmailFactory, + "googlechat": GoogleChatFactory, + "kafka": KafkaFactory, + "line": LineFactory, + "opsgenie": OpsgenieFactory, + "pagerduty": PagerdutyFactory, + "pushover": PushoverFactory, + "sensugo": SensuGoFactory, + "slack": SlackFactory, + "teams": TeamsFactory, + "telegram": TelegramFactory, + "threema": ThreemaFactory, + "victorops": VictorOpsFactory, + "webhook": WebHookFactory, + "wecom": WeComFactory, +} + +func Factory(receiverType string) (func(FactoryConfig) (NotificationChannel, error), bool) { + receiverType = strings.ToLower(receiverType) + factory, exists := receiverFactories[receiverType] + return factory, exists +} diff --git a/pkg/services/ngalert/notifier/channels/googlechat.go b/pkg/services/ngalert/notifier/channels/googlechat.go index f14c1f373e2..20003464722 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat.go +++ b/pkg/services/ngalert/notifier/channels/googlechat.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "fmt" "time" @@ -26,32 +27,50 @@ type GoogleChatNotifier struct { content string } -func NewGoogleChatNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*GoogleChatNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } +type GoogleChatConfig struct { + *NotificationChannelConfig + URL string + Content string +} - url := model.Settings.Get("url").MustString() +func GoogleChatFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewGoogleChatConfig(fc.Config) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewGoogleChatNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewGoogleChatConfig(config *NotificationChannelConfig) (*GoogleChatConfig, error) { + url := config.Settings.Get("url").MustString() if url == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"} + return nil, errors.New("could not find url property in settings") } + return &GoogleChatConfig{ + NotificationChannelConfig: config, + URL: url, + Content: config.Settings.Get("message").MustString(`{{ template "default.message" . }}`), + }, nil +} - content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`) - +func NewGoogleChatNotifier(config *GoogleChatConfig, ns notifications.WebhookSender, t *template.Template) *GoogleChatNotifier { return &GoogleChatNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - URL: url, + content: config.Content, + URL: config.URL, log: log.New("alerting.notifier.googlechat"), ns: ns, tmpl: t, - content: content, - }, nil + } } // Notify send an alert notification to Google Chat. diff --git a/pkg/services/ngalert/notifier/channels/googlechat_test.go b/pkg/services/ngalert/notifier/channels/googlechat_test.go index 527d77be16f..93455ae8a61 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat_test.go +++ b/pkg/services/ngalert/notifier/channels/googlechat_test.go @@ -149,7 +149,7 @@ func TestGoogleChatNotifier(t *testing.T) { }, { name: "Error in initing", settings: `{}`, - expInitError: `failed to validate receiver "googlechat_testing" of type "googlechat": could not find url property in settings`, + expInitError: `could not find url property in settings`, }, { name: "Customized message", settings: `{"url": "http://localhost", "message": "I'm a custom template and you have {{ len .Alerts.Firing }} firing alert."}`, @@ -273,7 +273,7 @@ func TestGoogleChatNotifier(t *testing.T) { } webhookSender := mockNotificationService() - pn, err := NewGoogleChatNotifier(m, webhookSender, tmpl) + cfg, err := NewGoogleChatConfig(m) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -283,6 +283,7 @@ func TestGoogleChatNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewGoogleChatNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/kafka.go b/pkg/services/ngalert/notifier/channels/kafka.go index 65983dbb366..feeb9564590 100644 --- a/pkg/services/ngalert/notifier/channels/kafka.go +++ b/pkg/services/ngalert/notifier/channels/kafka.go @@ -2,6 +2,7 @@ package channels import ( "context" + "errors" "strings" "github.com/prometheus/alertmanager/notify" @@ -26,35 +27,55 @@ type KafkaNotifier struct { tmpl *template.Template } -// NewKafkaNotifier is the constructor function for the Kafka notifier. -func NewKafkaNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*KafkaNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } +type KafkaConfig struct { + *NotificationChannelConfig + Endpoint string + Topic string +} - endpoint := model.Settings.Get("kafkaRestProxy").MustString() +func KafkaFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewKafkaConfig(fc.Config) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewKafkaNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewKafkaConfig(config *NotificationChannelConfig) (*KafkaConfig, error) { + endpoint := config.Settings.Get("kafkaRestProxy").MustString() if endpoint == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find kafka rest proxy endpoint property in settings"} + return nil, errors.New("could not find kafka rest proxy endpoint property in settings") } - topic := model.Settings.Get("kafkaTopic").MustString() + topic := config.Settings.Get("kafkaTopic").MustString() if topic == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find kafka topic property in settings"} + return nil, errors.New("could not find kafka topic property in settings") } + return &KafkaConfig{ + NotificationChannelConfig: config, + Endpoint: endpoint, + Topic: topic, + }, nil +} +// NewKafkaNotifier is the constructor function for the Kafka notifier. +func NewKafkaNotifier(config *KafkaConfig, ns notifications.WebhookSender, t *template.Template) *KafkaNotifier { return &KafkaNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - Endpoint: endpoint, - Topic: topic, + Endpoint: config.Endpoint, + Topic: config.Topic, log: log.New("alerting.notifier.kafka"), ns: ns, tmpl: t, - }, nil + } } // Notify sends the alert notification. diff --git a/pkg/services/ngalert/notifier/channels/kafka_test.go b/pkg/services/ngalert/notifier/channels/kafka_test.go index 34bf944e592..054431e52a9 100644 --- a/pkg/services/ngalert/notifier/channels/kafka_test.go +++ b/pkg/services/ngalert/notifier/channels/kafka_test.go @@ -96,11 +96,11 @@ func TestKafkaNotifier(t *testing.T) { }, { name: "Endpoint missing", settings: `{"kafkaTopic": "sometopic"}`, - expInitError: `failed to validate receiver "kafka_testing" of type "kafka": could not find kafka rest proxy endpoint property in settings`, + expInitError: `could not find kafka rest proxy endpoint property in settings`, }, { name: "Topic missing", settings: `{"kafkaRestProxy": "http://localhost"}`, - expInitError: `failed to validate receiver "kafka_testing" of type "kafka": could not find kafka topic property in settings`, + expInitError: `could not find kafka topic property in settings`, }, } @@ -116,7 +116,7 @@ func TestKafkaNotifier(t *testing.T) { } webhookSender := mockNotificationService() - pn, err := NewKafkaNotifier(m, webhookSender, tmpl) + cfg, err := NewKafkaConfig(m) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -126,6 +126,8 @@ func TestKafkaNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + + pn := NewKafkaNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/line.go b/pkg/services/ngalert/notifier/channels/line.go index 79f27180b43..06987ded24d 100644 --- a/pkg/services/ngalert/notifier/channels/line.go +++ b/pkg/services/ngalert/notifier/channels/line.go @@ -2,6 +2,7 @@ package channels import ( "context" + "errors" "fmt" "net/url" "path" @@ -17,33 +18,48 @@ var ( LineNotifyURL string = "https://notify-api.line.me/api/notify" ) -// NewLineNotifier is the constructor for the LINE notifier -func NewLineNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*LineNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } +type LineConfig struct { + *NotificationChannelConfig + Token string +} - token := fn(context.Background(), model.SecureSettings, "token", model.Settings.Get("token").MustString()) +func LineFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewLineConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewLineNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewLineConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*LineConfig, error) { + token := decryptFunc(context.Background(), config.SecureSettings, "token", config.Settings.Get("token").MustString()) if token == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find token in settings"} + return nil, errors.New("could not find token in settings") } + return &LineConfig{ + NotificationChannelConfig: config, + Token: token, + }, nil +} +// NewLineNotifier is the constructor for the LINE notifier +func NewLineNotifier(config *LineConfig, ns notifications.WebhookSender, t *template.Template) *LineNotifier { return &LineNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - Token: token, + Token: config.Token, log: log.New("alerting.notifier.line"), ns: ns, tmpl: t, - }, nil + } } // LineNotifier is responsible for sending diff --git a/pkg/services/ngalert/notifier/channels/line_test.go b/pkg/services/ngalert/notifier/channels/line_test.go index 467e6bf3621..259045d3d93 100644 --- a/pkg/services/ngalert/notifier/channels/line_test.go +++ b/pkg/services/ngalert/notifier/channels/line_test.go @@ -73,7 +73,7 @@ func TestLineNotifier(t *testing.T) { }, { name: "Token missing", settings: `{}`, - expInitError: `failed to validate receiver "line_testing" of type "line": could not find token in settings`, + expInitError: `could not find token in settings`, }, } @@ -93,7 +93,7 @@ func TestLineNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewLineNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewLineConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -103,6 +103,7 @@ func TestLineNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewLineNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/opsgenie.go b/pkg/services/ngalert/notifier/channels/opsgenie.go index c9067561f92..43149270a39 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "fmt" "net/http" "sort" @@ -41,49 +42,66 @@ type OpsgenieNotifier struct { ns notifications.WebhookSender } -// NewOpsgenieNotifier is the constructor for the Opsgenie notifier -func NewOpsgenieNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*OpsgenieNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } - autoClose := model.Settings.Get("autoClose").MustBool(true) - overridePriority := model.Settings.Get("overridePriority").MustBool(true) - apiKey := fn(context.Background(), model.SecureSettings, "apiKey", model.Settings.Get("apiKey").MustString()) - apiURL := model.Settings.Get("apiUrl").MustString() - if apiKey == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find api key property in settings"} - } - if apiURL == "" { - apiURL = OpsgenieAlertURL - } +type OpsgenieConfig struct { + *NotificationChannelConfig + APIKey string + APIUrl string + AutoClose bool + OverridePriority bool + SendTagsAs string +} - sendTagsAs := model.Settings.Get("sendTagsAs").MustString(OpsgenieSendTags) - if sendTagsAs != OpsgenieSendTags && sendTagsAs != OpsgenieSendDetails && sendTagsAs != OpsgenieSendBoth { - return nil, receiverInitError{Cfg: *model, - Reason: fmt.Sprintf("invalid value for sendTagsAs: %q", sendTagsAs), +func OpsgenieFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewOpsgenieConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, } } + return NewOpsgenieNotifier(cfg, fc.NotificationService, fc.Template, fc.DecryptFunc), nil +} +func NewOpsgenieConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*OpsgenieConfig, error) { + apiKey := decryptFunc(context.Background(), config.SecureSettings, "apiKey", config.Settings.Get("apiKey").MustString()) + if apiKey == "" { + return nil, errors.New("could not find api key property in settings") + } + sendTagsAs := config.Settings.Get("sendTagsAs").MustString(OpsgenieSendTags) + if sendTagsAs != OpsgenieSendTags && + sendTagsAs != OpsgenieSendDetails && + sendTagsAs != OpsgenieSendBoth { + return nil, fmt.Errorf("invalid value for sendTagsAs: %q", sendTagsAs) + } + return &OpsgenieConfig{ + NotificationChannelConfig: config, + APIKey: apiKey, + APIUrl: config.Settings.Get("apiUrl").MustString(OpsgenieAlertURL), + AutoClose: config.Settings.Get("autoClose").MustBool(true), + OverridePriority: config.Settings.Get("overridePriority").MustBool(true), + SendTagsAs: sendTagsAs, + }, nil +} + +// NewOpsgenieNotifier is the constructor for the Opsgenie notifier +func NewOpsgenieNotifier(config *OpsgenieConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) *OpsgenieNotifier { return &OpsgenieNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - APIKey: apiKey, - APIUrl: apiURL, - AutoClose: autoClose, - OverridePriority: overridePriority, - SendTagsAs: sendTagsAs, + APIKey: config.APIKey, + APIUrl: config.APIUrl, + AutoClose: config.AutoClose, + OverridePriority: config.OverridePriority, + SendTagsAs: config.SendTagsAs, tmpl: t, - log: log.New("alerting.notifier." + model.Name), + log: log.New("alerting.notifier." + config.Name), ns: ns, - }, nil + } } // Notify sends an alert notification to Opsgenie diff --git a/pkg/services/ngalert/notifier/channels/opsgenie_test.go b/pkg/services/ngalert/notifier/channels/opsgenie_test.go index 5dbb8db481a..818f86f0ed7 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie_test.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie_test.go @@ -153,7 +153,7 @@ func TestOpsgenieNotifier(t *testing.T) { { name: "Error when incorrect settings", settings: `{}`, - expInitError: `failed to validate receiver "opsgenie_testing" of type "opsgenie": could not find api key property in settings`, + expInitError: `could not find api key property in settings`, }, } @@ -174,7 +174,7 @@ func TestOpsgenieNotifier(t *testing.T) { webhookSender.Webhook.Body = "" secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewOpsgenieNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewOpsgenieConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -184,6 +184,7 @@ func TestOpsgenieNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewOpsgenieNotifier(cfg, webhookSender, tmpl, decryptFn) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index 51e5fd51347..1775fc7eac1 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "fmt" "os" @@ -40,44 +41,69 @@ type PagerdutyNotifier struct { ns notifications.WebhookSender } -// NewPagerdutyNotifier is the constructor for the PagerDuty notifier -func NewPagerdutyNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*PagerdutyNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } +type PagerdutyConfig struct { + *NotificationChannelConfig + Key string + Severity string + Class string + Component string + Group string + Summary string +} - key := fn(context.Background(), model.SecureSettings, "integrationKey", model.Settings.Get("integrationKey").MustString()) +func PagerdutyFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewPagerdutyConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewPagerdutyNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewPagerdutyConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*PagerdutyConfig, error) { + key := decryptFunc(context.Background(), config.SecureSettings, "integrationKey", config.Settings.Get("integrationKey").MustString()) if key == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find integration key property in settings"} + return nil, errors.New("could not find integration key property in settings") } + return &PagerdutyConfig{ + NotificationChannelConfig: config, + Key: key, + Severity: config.Settings.Get("severity").MustString("critical"), + Class: config.Settings.Get("class").MustString("default"), + Component: config.Settings.Get("component").MustString("Grafana"), + Group: config.Settings.Get("group").MustString("default"), + Summary: config.Settings.Get("summary").MustString(DefaultMessageTitleEmbed), + }, nil +} +// NewPagerdutyNotifier is the constructor for the PagerDuty notifier +func NewPagerdutyNotifier(config *PagerdutyConfig, ns notifications.WebhookSender, t *template.Template) *PagerdutyNotifier { return &PagerdutyNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - Key: key, + Key: config.Key, CustomDetails: map[string]string{ "firing": `{{ template "__text_alert_list" .Alerts.Firing }}`, "resolved": `{{ template "__text_alert_list" .Alerts.Resolved }}`, "num_firing": `{{ .Alerts.Firing | len }}`, "num_resolved": `{{ .Alerts.Resolved | len }}`, }, - Severity: model.Settings.Get("severity").MustString("critical"), - Class: model.Settings.Get("class").MustString("default"), - Component: model.Settings.Get("component").MustString("Grafana"), - Group: model.Settings.Get("group").MustString("default"), - Summary: model.Settings.Get("summary").MustString(DefaultMessageTitleEmbed), + Severity: config.Severity, + Class: config.Class, + Component: config.Component, + Group: config.Group, + Summary: config.Summary, tmpl: t, - log: log.New("alerting.notifier." + model.Name), + log: log.New("alerting.notifier." + config.Name), ns: ns, - }, nil + } } // Notify sends an alert notification to PagerDuty diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go index 44e1e3bffc6..a9de7a25912 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty_test.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -119,7 +119,7 @@ func TestPagerdutyNotifier(t *testing.T) { }, { name: "Error in initing", settings: `{}`, - expInitError: `failed to validate receiver "pageduty_testing" of type "pagerduty": could not find integration key property in settings`, + expInitError: `could not find integration key property in settings`, }, } @@ -139,7 +139,7 @@ func TestPagerdutyNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewPagerdutyNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewPagerdutyConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -149,6 +149,7 @@ func TestPagerdutyNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewPagerdutyNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/pushover.go b/pkg/services/ngalert/notifier/channels/pushover.go index f684ba8ceaa..ee0eae6e2a0 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" + "errors" "fmt" "mime/multipart" "strconv" @@ -39,62 +40,93 @@ type PushoverNotifier struct { ns notifications.WebhookSender } -// NewSlackNotifier is the constructor for the Slack notifier -func NewPushoverNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*PushoverNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } +type PushoverConfig struct { + *NotificationChannelConfig + UserKey string + APIToken string + AlertingPriority int + OKPriority int + Retry int + Expire int + Device string + AlertingSound string + OKSound string + Upload bool + Message string +} - userKey := fn(context.Background(), model.SecureSettings, "userKey", model.Settings.Get("userKey").MustString()) - APIToken := fn(context.Background(), model.SecureSettings, "apiToken", model.Settings.Get("apiToken").MustString()) - device := model.Settings.Get("device").MustString() - alertingPriority, err := strconv.Atoi(model.Settings.Get("priority").MustString("0")) // default Normal +func PushoverFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewPushoverConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewPushoverNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewPushoverConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*PushoverConfig, error) { + userKey := decryptFunc(context.Background(), config.SecureSettings, "userKey", config.Settings.Get("userKey").MustString()) + if userKey == "" { + return nil, errors.New("user key not found") + } + APIToken := decryptFunc(context.Background(), config.SecureSettings, "apiToken", config.Settings.Get("apiToken").MustString()) + if APIToken == "" { + return nil, errors.New("API token not found") + } + alertingPriority, err := strconv.Atoi(config.Settings.Get("priority").MustString("0")) // default Normal if err != nil { return nil, fmt.Errorf("failed to convert alerting priority to integer: %w", err) } - okPriority, err := strconv.Atoi(model.Settings.Get("okPriority").MustString("0")) // default Normal + okPriority, err := strconv.Atoi(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(model.Settings.Get("retry").MustString()) - expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString()) - alertingSound := model.Settings.Get("sound").MustString() - okSound := model.Settings.Get("okSound").MustString() - uploadImage := model.Settings.Get("uploadImage").MustBool(true) + retry, _ := strconv.Atoi(config.Settings.Get("retry").MustString()) + expire, _ := strconv.Atoi(config.Settings.Get("expire").MustString()) + return &PushoverConfig{ + NotificationChannelConfig: config, + APIToken: APIToken, + UserKey: userKey, + AlertingPriority: alertingPriority, + OKPriority: okPriority, + Retry: retry, + Expire: expire, + Device: config.Settings.Get("device").MustString(), + AlertingSound: config.Settings.Get("sound").MustString(), + OKSound: config.Settings.Get("okSound").MustString(), + Upload: config.Settings.Get("uploadImage").MustBool(true), + Message: config.Settings.Get("message").MustString(`{{ template "default.message" .}}`), + }, nil +} - if userKey == "" { - return nil, receiverInitError{Cfg: *model, Reason: "user key not found"} - } - if APIToken == "" { - return nil, receiverInitError{Cfg: *model, Reason: "API token not found"} - } +// NewSlackNotifier is the constructor for the Slack notifier +func NewPushoverNotifier(config *PushoverConfig, ns notifications.WebhookSender, t *template.Template) *PushoverNotifier { return &PushoverNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, - SecureSettings: model.SecureSettings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, + SecureSettings: config.SecureSettings, }), - UserKey: userKey, - APIToken: APIToken, - AlertingPriority: alertingPriority, - OKPriority: okPriority, - Retry: retry, - Expire: expire, - Device: device, - AlertingSound: alertingSound, - OKSound: okSound, - Upload: uploadImage, - Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`), + UserKey: config.UserKey, + APIToken: config.APIToken, + AlertingPriority: config.AlertingPriority, + OKPriority: config.OKPriority, + Retry: config.Retry, + Expire: config.Expire, + Device: config.Device, + AlertingSound: config.AlertingSound, + OKSound: config.OKSound, + Upload: config.Upload, + Message: config.Message, tmpl: t, log: log.New("alerting.notifier.pushover"), ns: ns, - }, nil + } } // Notify sends an alert notification to Slack. diff --git a/pkg/services/ngalert/notifier/channels/pushover_test.go b/pkg/services/ngalert/notifier/channels/pushover_test.go index b9543720ff2..29e1a35e33d 100644 --- a/pkg/services/ngalert/notifier/channels/pushover_test.go +++ b/pkg/services/ngalert/notifier/channels/pushover_test.go @@ -112,13 +112,13 @@ func TestPushoverNotifier(t *testing.T) { settings: `{ "apiToken": "" }`, - expInitError: `failed to validate receiver "pushover_testing" of type "pushover": user key not found`, + expInitError: `user key not found`, }, { name: "Missing api key", settings: `{ "userKey": "" }`, - expInitError: `failed to validate receiver "pushover_testing" of type "pushover": API token not found`, + expInitError: `API token not found`, }, } @@ -147,7 +147,7 @@ func TestPushoverNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewPushoverNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewPushoverConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -157,6 +157,7 @@ func TestPushoverNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewPushoverNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.Error(t, err) diff --git a/pkg/services/ngalert/notifier/channels/sensugo.go b/pkg/services/ngalert/notifier/channels/sensugo.go index c6e6a4b05e2..b2a99310d05 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo.go +++ b/pkg/services/ngalert/notifier/channels/sensugo.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "fmt" "strings" @@ -29,44 +30,71 @@ type SensuGoNotifier struct { Message string } -// NewSensuGoNotifier is the constructor for the SensuGo notifier -func NewSensuGoNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*SensuGoNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} +type SensuGoConfig struct { + *NotificationChannelConfig + URL string + Entity string + Check string + Namespace string + Handler string + APIKey string + Message string +} + +func SensuGoFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewSensuGoConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } - url := model.Settings.Get("url").MustString() + return NewSensuGoNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewSensuGoConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*SensuGoConfig, error) { + url := config.Settings.Get("url").MustString() if url == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find URL property in settings"} + return nil, errors.New("could not find URL property in settings") } - - apikey := fn(context.Background(), model.SecureSettings, "apikey", model.Settings.Get("apikey").MustString()) + apikey := decryptFunc(context.Background(), config.SecureSettings, "apikey", config.Settings.Get("apikey").MustString()) if apikey == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find the API key property in settings"} + 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(`{{ template "default.message" .}}`), + }, nil +} +// NewSensuGoNotifier is the constructor for the SensuGo notifier +func NewSensuGoNotifier(config *SensuGoConfig, ns notifications.WebhookSender, t *template.Template) *SensuGoNotifier { return &SensuGoNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, - SecureSettings: model.SecureSettings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, + SecureSettings: config.SecureSettings, }), - URL: url, - Entity: model.Settings.Get("entity").MustString(), - Check: model.Settings.Get("check").MustString(), - Namespace: model.Settings.Get("namespace").MustString(), - Handler: model.Settings.Get("handler").MustString(), - APIKey: apikey, - Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`), + 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"), ns: ns, tmpl: t, - }, nil + } } // Notify sends an alert notification to Sensu Go diff --git a/pkg/services/ngalert/notifier/channels/sensugo_test.go b/pkg/services/ngalert/notifier/channels/sensugo_test.go index 271265c1d30..1a769c1de8b 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo_test.go +++ b/pkg/services/ngalert/notifier/channels/sensugo_test.go @@ -121,13 +121,13 @@ func TestSensuGoNotifier(t *testing.T) { settings: `{ "apikey": "" }`, - expInitError: `failed to validate receiver "Sensu Go" of type "sensugo": could not find URL property in settings`, + expInitError: `could not find URL property in settings`, }, { name: "Error in initing: missing API key", settings: `{ "url": "http://sensu-api.local:8080" }`, - expInitError: `failed to validate receiver "Sensu Go" of type "sensugo": could not find the API key property in settings`, + expInitError: `could not find the API key property in settings`, }, } @@ -147,7 +147,7 @@ func TestSensuGoNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - sn, err := NewSensuGoNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewSensuGoConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -157,6 +157,7 @@ func TestSensuGoNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + sn := NewSensuGoNotifier(cfg, 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/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index e77ef66afec..05fe3a48b61 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "io" "net" @@ -21,6 +22,8 @@ import ( "github.com/prometheus/alertmanager/types" ) +var SlackAPIEndpoint = "https://slack.com/api/chat.postMessage" + // SlackNotifier is responsible for sending // alert notification to Slack. type SlackNotifier struct { @@ -41,43 +44,55 @@ type SlackNotifier struct { Token string } -var SlackAPIEndpoint = "https://slack.com/api/chat.postMessage" +type SlackConfig struct { + *NotificationChannelConfig + URL *url.URL + Username string + IconEmoji string + IconURL string + Recipient string + Text string + Title string + MentionUsers []string + MentionGroups []string + MentionChannel string + Token string +} -// NewSlackNotifier is the constructor for the Slack notifier -func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn GetDecryptedValueFn) (*SlackNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} +func SlackFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewSlackConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } } + return NewSlackNotifier(cfg, fc.Template), nil +} - endpointURL := model.Settings.Get("endpointUrl").MustString(SlackAPIEndpoint) - - slackURL := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString()) +func NewSlackConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*SlackConfig, error) { + endpointURL := config.Settings.Get("endpointUrl").MustString(SlackAPIEndpoint) + slackURL := decryptFunc(context.Background(), config.SecureSettings, "url", config.Settings.Get("url").MustString()) if slackURL == "" { slackURL = endpointURL } apiURL, err := url.Parse(slackURL) if err != nil { - return nil, receiverInitError{Cfg: *model, Reason: fmt.Sprintf("invalid URL %q", slackURL), Err: err} + return nil, fmt.Errorf("invalid URL %q", slackURL) } - - recipient := strings.TrimSpace(model.Settings.Get("recipient").MustString()) - if recipient == "" && apiURL.String() == endpointURL { - return nil, receiverInitError{Cfg: *model, - Reason: "recipient must be specified when using the Slack chat API", - } + recipient := strings.TrimSpace(config.Settings.Get("recipient").MustString()) + if recipient == "" && apiURL.String() == SlackAPIEndpoint { + return nil, errors.New("recipient must be specified when using the Slack chat API") } - - mentionChannel := model.Settings.Get("mentionChannel").MustString() + mentionChannel := config.Settings.Get("mentionChannel").MustString() if mentionChannel != "" && mentionChannel != "here" && mentionChannel != "channel" { - return nil, receiverInitError{Cfg: *model, - Reason: fmt.Sprintf("invalid value for mentionChannel: %q", mentionChannel), - } + return nil, fmt.Errorf("invalid value for mentionChannel: %q", mentionChannel) } - - mentionUsersStr := model.Settings.Get("mentionUsers").MustString() + token := decryptFunc(context.Background(), config.SecureSettings, "token", config.Settings.Get("token").MustString()) + if token == "" && apiURL.String() == SlackAPIEndpoint { + return nil, errors.New("token must be specified when using the Slack chat API") + } + mentionUsersStr := config.Settings.Get("mentionUsers").MustString() mentionUsers := []string{} for _, u := range strings.Split(mentionUsersStr, ",") { u = strings.TrimSpace(u) @@ -85,8 +100,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn mentionUsers = append(mentionUsers, u) } } - - mentionGroupsStr := model.Settings.Get("mentionGroups").MustString() + mentionGroupsStr := config.Settings.Get("mentionGroups").MustString() mentionGroups := []string{} for _, g := range strings.Split(mentionGroupsStr, ",") { g = strings.TrimSpace(g) @@ -94,36 +108,46 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn mentionGroups = append(mentionGroups, g) } } + return &SlackConfig{ + NotificationChannelConfig: config, + Recipient: strings.TrimSpace(config.Settings.Get("recipient").MustString()), + MentionChannel: config.Settings.Get("mentionChannel").MustString(), + MentionUsers: mentionUsers, + MentionGroups: mentionGroups, + URL: apiURL, + Username: config.Settings.Get("username").MustString("Grafana"), + IconEmoji: config.Settings.Get("icon_emoji").MustString(), + IconURL: config.Settings.Get("icon_url").MustString(), + Token: token, + Text: config.Settings.Get("text").MustString(`{{ template "default.message" . }}`), + Title: config.Settings.Get("title").MustString(DefaultMessageTitleEmbed), + }, nil +} - token := fn(context.Background(), model.SecureSettings, "token", model.Settings.Get("token").MustString()) - if token == "" && apiURL.String() == SlackAPIEndpoint { - return nil, receiverInitError{Cfg: *model, - Reason: "token must be specified when using the Slack chat API", - } - } - +// NewSlackNotifier is the constructor for the Slack notifier +func NewSlackNotifier(config *SlackConfig, t *template.Template) *SlackNotifier { return &SlackNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - URL: apiURL, - Recipient: recipient, - MentionUsers: mentionUsers, - MentionGroups: mentionGroups, - MentionChannel: mentionChannel, - Username: model.Settings.Get("username").MustString("Grafana"), - IconEmoji: model.Settings.Get("icon_emoji").MustString(), - IconURL: model.Settings.Get("icon_url").MustString(), - Token: token, - Text: model.Settings.Get("text").MustString(`{{ template "default.message" . }}`), - Title: model.Settings.Get("title").MustString(DefaultMessageTitleEmbed), + URL: config.URL, + Recipient: config.Recipient, + MentionUsers: config.MentionUsers, + MentionGroups: config.MentionGroups, + MentionChannel: config.MentionChannel, + Username: config.Username, + IconEmoji: config.IconEmoji, + IconURL: config.IconURL, + Token: config.Token, + Text: config.Text, + Title: config.Title, log: log.New("alerting.notifier.slack"), tmpl: t, - }, nil + } } // slackMessage is the slackMessage for sending a slack notification. diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index 62419671619..2389d7f85aa 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -152,13 +152,13 @@ func TestSlackNotifier(t *testing.T) { settings: `{ "recipient": "#testchannel" }`, - expInitError: `failed to validate receiver "slack_testing" of type "slack": token must be specified when using the Slack chat API`, + expInitError: `token must be specified when using the Slack chat API`, }, { name: "Missing recipient", settings: `{ "token": "1234" }`, - expInitError: `failed to validate receiver "slack_testing" of type "slack": recipient must be specified when using the Slack chat API`, + expInitError: `recipient must be specified when using the Slack chat API`, }, { name: "Custom endpoint url", @@ -213,7 +213,7 @@ func TestSlackNotifier(t *testing.T) { secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewSlackNotifier(m, tmpl, decryptFn) + cfg, err := NewSlackConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -246,6 +246,7 @@ func TestSlackNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewSlackNotifier(cfg, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.Error(t, err) diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index 509b6e10844..e03a48bf8d7 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -24,31 +24,51 @@ type TeamsNotifier struct { ns notifications.WebhookSender } +type TeamsConfig struct { + *NotificationChannelConfig + URL string + Message 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.Template), nil +} + +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") + } + return &TeamsConfig{ + NotificationChannelConfig: config, + URL: URL, + Message: config.Settings.Get("message").MustString(`{{ template "teams.default.message" .}}`), + }, nil +} + // NewTeamsNotifier is the constructor for Teams notifier. -func NewTeamsNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*TeamsNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - - u := model.Settings.Get("url").MustString() - if u == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"} - } - +func NewTeamsNotifier(config *TeamsConfig, ns notifications.WebhookSender, t *template.Template) *TeamsNotifier { return &TeamsNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - URL: u, - Message: model.Settings.Get("message").MustString(`{{ template "teams.default.message" .}}`), + URL: config.URL, + Message: config.Message, log: log.New("alerting.notifier.teams"), ns: ns, tmpl: t, - }, nil + } } // Notify send an alert notification to Microsoft teams. diff --git a/pkg/services/ngalert/notifier/channels/teams_test.go b/pkg/services/ngalert/notifier/channels/teams_test.go index 7f743f6fe91..1d09c78661c 100644 --- a/pkg/services/ngalert/notifier/channels/teams_test.go +++ b/pkg/services/ngalert/notifier/channels/teams_test.go @@ -106,7 +106,7 @@ func TestTeamsNotifier(t *testing.T) { }, { name: "Error in initing", settings: `{}`, - expInitError: `failed to validate receiver "teams_testing" of type "teams": could not find url property in settings`, + expInitError: `could not find url property in settings`, }, } @@ -122,7 +122,7 @@ func TestTeamsNotifier(t *testing.T) { } webhookSender := mockNotificationService() - pn, err := NewTeamsNotifier(m, webhookSender, tmpl) + cfg, err := NewTeamsConfig(m) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -132,6 +132,7 @@ func TestTeamsNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewTeamsNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/telegram.go b/pkg/services/ngalert/notifier/channels/telegram.go index a9d6e75b1a4..7c614276115 100644 --- a/pkg/services/ngalert/notifier/channels/telegram.go +++ b/pkg/services/ngalert/notifier/channels/telegram.go @@ -3,6 +3,7 @@ package channels import ( "bytes" "context" + "errors" "fmt" "mime/multipart" @@ -29,42 +30,58 @@ type TelegramNotifier struct { tmpl *template.Template } -// NewTelegramNotifier is the constructor for the Telegram notifier -func NewTelegramNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*TelegramNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } +type TelegramConfig struct { + *NotificationChannelConfig + BotToken string + ChatID string + Message string +} - botToken := fn(context.Background(), model.SecureSettings, "bottoken", model.Settings.Get("bottoken").MustString()) - chatID := model.Settings.Get("chatid").MustString() - message := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`) +func TelegramFactory(fc FactoryConfig) (NotificationChannel, error) { + config, err := NewTelegramConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewTelegramNotifier(config, fc.NotificationService, fc.Template), nil +} +func NewTelegramConfig(config *NotificationChannelConfig, fn GetDecryptedValueFn) (*TelegramConfig, error) { + botToken := fn(context.Background(), config.SecureSettings, "bottoken", config.Settings.Get("bottoken").MustString()) if botToken == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find Bot Token in settings"} + return &TelegramConfig{}, errors.New("could not find Bot Token in settings") } - + chatID := config.Settings.Get("chatid").MustString() if chatID == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find Chat Id in settings"} + return &TelegramConfig{}, errors.New("could not find Chat Id in settings") } + return &TelegramConfig{ + NotificationChannelConfig: config, + BotToken: botToken, + ChatID: chatID, + Message: config.Settings.Get("message").MustString(`{{ template "default.message" . }}`), + }, nil +} +// NewTelegramNotifier is the constructor for the Telegram notifier +func NewTelegramNotifier(config *TelegramConfig, ns notifications.WebhookSender, t *template.Template) *TelegramNotifier { return &TelegramNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - BotToken: botToken, - ChatID: chatID, - Message: message, + BotToken: config.BotToken, + ChatID: config.ChatID, + Message: config.Message, tmpl: t, log: log.New("alerting.notifier.telegram"), ns: ns, - }, nil + } } // Notify send an alert notification to Telegram. diff --git a/pkg/services/ngalert/notifier/channels/telegram_test.go b/pkg/services/ngalert/notifier/channels/telegram_test.go index c3d7c9ba921..32b55458211 100644 --- a/pkg/services/ngalert/notifier/channels/telegram_test.go +++ b/pkg/services/ngalert/notifier/channels/telegram_test.go @@ -81,7 +81,7 @@ func TestTelegramNotifier(t *testing.T) { }, { name: "Error in initing", settings: `{}`, - expInitError: `failed to validate receiver "telegram_testing" of type "telegram": could not find Bot Token in settings`, + expInitError: `could not find Bot Token in settings`, }, } @@ -101,16 +101,16 @@ func TestTelegramNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewTelegramNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewTelegramConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) return } require.NoError(t, err) - ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewTelegramNotifier(cfg, webhookSender, tmpl) msg, err := pn.buildTelegramMessage(ctx, c.alerts) if c.expMsgError != nil { require.Error(t, err) diff --git a/pkg/services/ngalert/notifier/channels/threema.go b/pkg/services/ngalert/notifier/channels/threema.go index 8fd6bbc6b1b..5f4444b043d 100644 --- a/pkg/services/ngalert/notifier/channels/threema.go +++ b/pkg/services/ngalert/notifier/channels/threema.go @@ -2,6 +2,7 @@ package channels import ( "context" + "errors" "fmt" "net/url" "path" @@ -31,53 +32,71 @@ type ThreemaNotifier struct { tmpl *template.Template } -// NewThreemaNotifier is the constructor for the Threema notifier -func NewThreemaNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*ThreemaNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } - gatewayID := model.Settings.Get("gateway_id").MustString() - recipientID := model.Settings.Get("recipient_id").MustString() - apiSecret := fn(context.Background(), model.SecureSettings, "api_secret", model.Settings.Get("api_secret").MustString()) +type ThreemaConfig struct { + *NotificationChannelConfig + GatewayID string + RecipientID string + APISecret string +} - // Validation +func ThreemaFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewThreemaConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewThreemaNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewThreemaConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*ThreemaConfig, error) { + gatewayID := config.Settings.Get("gateway_id").MustString() if gatewayID == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find Threema Gateway ID in settings"} + return nil, errors.New("could not find Threema Gateway ID in settings") } if !strings.HasPrefix(gatewayID, "*") { - return nil, receiverInitError{Cfg: *model, Reason: "invalid Threema Gateway ID: Must start with a *"} + return nil, errors.New("invalid Threema Gateway ID: Must start with a *") } if len(gatewayID) != 8 { - return nil, receiverInitError{Cfg: *model, Reason: "invalid Threema Gateway ID: Must be 8 characters long"} + return nil, errors.New("invalid Threema Gateway ID: Must be 8 characters long") } + recipientID := config.Settings.Get("recipient_id").MustString() if recipientID == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find Threema Recipient ID in settings"} + return nil, errors.New("could not find Threema Recipient ID in settings") } if len(recipientID) != 8 { - return nil, receiverInitError{Cfg: *model, Reason: "invalid Threema Recipient ID: Must be 8 characters long"} + return nil, errors.New("invalid Threema Recipient ID: Must be 8 characters long") } + apiSecret := decryptFunc(context.Background(), config.SecureSettings, "api_secret", config.Settings.Get("api_secret").MustString()) if apiSecret == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find Threema API secret in settings"} + return nil, errors.New("could not find Threema API secret in settings") } + return &ThreemaConfig{ + NotificationChannelConfig: config, + GatewayID: gatewayID, + RecipientID: recipientID, + APISecret: apiSecret, + }, nil +} +// NewThreemaNotifier is the constructor for the Threema notifier +func NewThreemaNotifier(config *ThreemaConfig, ns notifications.WebhookSender, t *template.Template) *ThreemaNotifier { return &ThreemaNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - GatewayID: gatewayID, - RecipientID: recipientID, - APISecret: apiSecret, + GatewayID: config.GatewayID, + RecipientID: config.RecipientID, + APISecret: config.APISecret, log: log.New("alerting.notifier.threema"), ns: ns, tmpl: t, - }, nil + } } // Notify send an alert notification to Threema diff --git a/pkg/services/ngalert/notifier/channels/threema_test.go b/pkg/services/ngalert/notifier/channels/threema_test.go index 47364e677f2..c59dc60a6b4 100644 --- a/pkg/services/ngalert/notifier/channels/threema_test.go +++ b/pkg/services/ngalert/notifier/channels/threema_test.go @@ -76,7 +76,7 @@ func TestThreemaNotifier(t *testing.T) { "recipient_id": "87654321", "api_secret": "supersecret" }`, - expInitError: `failed to validate receiver "threema_testing" of type "threema": invalid Threema Gateway ID: Must start with a *`, + expInitError: `invalid Threema Gateway ID: Must start with a *`, }, { name: "Invalid receipent id", settings: `{ @@ -84,14 +84,14 @@ func TestThreemaNotifier(t *testing.T) { "recipient_id": "8765432", "api_secret": "supersecret" }`, - expInitError: `failed to validate receiver "threema_testing" of type "threema": invalid Threema Recipient ID: Must be 8 characters long`, + expInitError: `invalid Threema Recipient ID: Must be 8 characters long`, }, { name: "No API secret", settings: `{ "gateway_id": "*1234567", "recipient_id": "87654321" }`, - expInitError: `failed to validate receiver "threema_testing" of type "threema": could not find Threema API secret in settings`, + expInitError: `could not find Threema API secret in settings`, }, } @@ -111,7 +111,7 @@ func TestThreemaNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewThreemaNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewThreemaConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -121,6 +121,7 @@ func TestThreemaNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewThreemaNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/utils.go b/pkg/services/ngalert/notifier/channels/utils.go index aebdc360eb6..4334a2a5262 100644 --- a/pkg/services/ngalert/notifier/channels/utils.go +++ b/pkg/services/ngalert/notifier/channels/utils.go @@ -12,6 +12,7 @@ import ( "path" "time" + "github.com/prometheus/alertmanager/notify" "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/infra/log" @@ -60,6 +61,10 @@ func getAlertStatusColor(status model.AlertStatus) string { return ColorAlertResolved } +type NotificationChannel interface { + notify.Notifier + notify.ResolvedSender +} type NotificationChannelConfig struct { OrgID int64 // only used internally UID string `json:"uid"` diff --git a/pkg/services/ngalert/notifier/channels/victorops.go b/pkg/services/ngalert/notifier/channels/victorops.go index aba0167965f..12f9fb0c7fc 100644 --- a/pkg/services/ngalert/notifier/channels/victorops.go +++ b/pkg/services/ngalert/notifier/channels/victorops.go @@ -2,6 +2,7 @@ package channels import ( "context" + "errors" "strings" "time" @@ -25,32 +26,52 @@ const ( victoropsAlertStateRecovery = "RECOVERY" ) +type VictorOpsConfig struct { + *NotificationChannelConfig + URL string + MessageType string +} + +func VictorOpsFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewVictorOpsConfig(fc.Config) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewVictoropsNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewVictorOpsConfig(config *NotificationChannelConfig) (*VictorOpsConfig, error) { + url := config.Settings.Get("url").MustString() + if url == "" { + return nil, errors.New("could not find victorops url property in settings") + } + return &VictorOpsConfig{ + NotificationChannelConfig: config, + URL: url, + MessageType: config.Settings.Get("messageType").MustString(), + }, nil +} + // NewVictoropsNotifier creates an instance of VictoropsNotifier that // handles posting notifications to Victorops REST API -func NewVictoropsNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template) (*VictoropsNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - - url := model.Settings.Get("url").MustString() - if url == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find victorops url property in settings"} - } - +func NewVictoropsNotifier(config *VictorOpsConfig, ns notifications.WebhookSender, t *template.Template) *VictoropsNotifier { return &VictoropsNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - URL: url, - MessageType: model.Settings.Get("messageType").MustString(), + URL: config.URL, + MessageType: config.MessageType, log: log.New("alerting.notifier.victorops"), ns: ns, tmpl: t, - }, nil + } } // VictoropsNotifier defines URL property for Victorops REST API diff --git a/pkg/services/ngalert/notifier/channels/victorops_test.go b/pkg/services/ngalert/notifier/channels/victorops_test.go index 3f11e25429b..f0c8740f9c1 100644 --- a/pkg/services/ngalert/notifier/channels/victorops_test.go +++ b/pkg/services/ngalert/notifier/channels/victorops_test.go @@ -78,7 +78,7 @@ func TestVictoropsNotifier(t *testing.T) { }, { name: "Error in initing, no URL", settings: `{}`, - expInitError: `failed to validate receiver "victorops_testing" of type "victorops": could not find victorops url property in settings`, + expInitError: `could not find victorops url property in settings`, }, } @@ -94,7 +94,7 @@ func TestVictoropsNotifier(t *testing.T) { } webhookSender := mockNotificationService() - pn, err := NewVictoropsNotifier(m, webhookSender, tmpl) + cfg, err := NewVictorOpsConfig(m) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -104,6 +104,7 @@ func TestVictoropsNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewVictoropsNotifier(cfg, webhookSender, 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 5422816d7b8..892dff3061a 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -28,37 +29,62 @@ type WebhookNotifier struct { orgID int64 } +type WebhookConfig struct { + *NotificationChannelConfig + URL string + User string + Password string + HTTPMethod string + MaxAlerts int +} + +func WebHookFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewWebHookConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } + } + return NewWebHookNotifier(cfg, fc.NotificationService, fc.Template), nil +} + +func NewWebHookConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*WebhookConfig, error) { + url := config.Settings.Get("url").MustString() + if url == "" { + return nil, errors.New("could not find url property in settings") + } + return &WebhookConfig{ + NotificationChannelConfig: config, + URL: url, + User: config.Settings.Get("username").MustString(), + Password: decryptFunc(context.Background(), config.SecureSettings, "password", config.Settings.Get("password").MustString()), + HTTPMethod: config.Settings.Get("httpMethod").MustString("POST"), + MaxAlerts: config.Settings.Get("maxAlerts").MustInt(0), + }, nil +} + // NewWebHookNotifier is the constructor for // the WebHook notifier. -func NewWebHookNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*WebhookNotifier, error) { - if model.Settings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no settings supplied"} - } - if model.SecureSettings == nil { - return nil, receiverInitError{Cfg: *model, Reason: "no secure settings supplied"} - } - url := model.Settings.Get("url").MustString() - if url == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"} - } +func NewWebHookNotifier(config *WebhookConfig, ns notifications.WebhookSender, t *template.Template) *WebhookNotifier { return &WebhookNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - orgID: model.OrgID, - URL: url, - User: model.Settings.Get("username").MustString(), - Password: fn(context.Background(), model.SecureSettings, "password", model.Settings.Get("password").MustString()), - HTTPMethod: model.Settings.Get("httpMethod").MustString("POST"), - MaxAlerts: model.Settings.Get("maxAlerts").MustInt(0), + orgID: config.OrgID, + URL: config.URL, + User: config.User, + Password: config.Password, + HTTPMethod: config.HTTPMethod, + MaxAlerts: config.MaxAlerts, log: log.New("alerting.notifier.webhook"), ns: ns, tmpl: t, - }, nil + } } // webhookMessage defines the JSON object send to webhook endpoints. diff --git a/pkg/services/ngalert/notifier/channels/webhook_test.go b/pkg/services/ngalert/notifier/channels/webhook_test.go index 97de78fe33c..a7bc50f3993 100644 --- a/pkg/services/ngalert/notifier/channels/webhook_test.go +++ b/pkg/services/ngalert/notifier/channels/webhook_test.go @@ -172,7 +172,7 @@ func TestWebhookNotifier(t *testing.T) { }, { name: "Error in initing", settings: `{}`, - expInitError: `failed to validate receiver "webhook_testing" of type "webhook": could not find url property in settings`, + expInitError: `could not find url property in settings`, }, } @@ -193,7 +193,7 @@ func TestWebhookNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewWebHookNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewWebHookConfig(m, decryptFn) if c.expInitError != "" { require.Error(t, err) require.Equal(t, c.expInitError, err.Error()) @@ -204,6 +204,7 @@ func TestWebhookNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) ctx = notify.WithReceiverName(ctx, "my_receiver") + pn := NewWebHookNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/ngalert/notifier/channels/wecom.go b/pkg/services/ngalert/notifier/channels/wecom.go index 6d135f9ddc5..e651a8c9973 100644 --- a/pkg/services/ngalert/notifier/channels/wecom.go +++ b/pkg/services/ngalert/notifier/channels/wecom.go @@ -3,6 +3,7 @@ package channels import ( "context" "encoding/json" + "errors" "fmt" "github.com/prometheus/alertmanager/types" @@ -13,28 +14,51 @@ import ( "github.com/prometheus/alertmanager/template" ) -// NewWeComNotifier is the constructor for WeCom notifier. -func NewWeComNotifier(model *NotificationChannelConfig, ns notifications.WebhookSender, t *template.Template, fn GetDecryptedValueFn) (*WeComNotifier, error) { - url := fn(context.Background(), model.SecureSettings, "url", model.Settings.Get("url").MustString()) +type WeComConfig struct { + *NotificationChannelConfig + URL string + Message string +} - if url == "" { - return nil, receiverInitError{Cfg: *model, Reason: "could not find webhook URL in settings"} +func WeComFactory(fc FactoryConfig) (NotificationChannel, error) { + cfg, err := NewWeComConfig(fc.Config, fc.DecryptFunc) + if err != nil { + return nil, receiverInitError{ + Reason: err.Error(), + Cfg: *fc.Config, + } } + return NewWeComNotifier(cfg, fc.NotificationService, fc.Template), nil +} +func NewWeComConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*WeComConfig, error) { + url := decryptFunc(context.Background(), config.SecureSettings, "url", config.Settings.Get("url").MustString()) + if url == "" { + return nil, errors.New("could not find webhook URL in settings") + } + return &WeComConfig{ + NotificationChannelConfig: config, + URL: url, + Message: config.Settings.Get("message").MustString(`{{ template "default.message" .}}`), + }, nil +} + +// NewWeComNotifier is the constructor for WeCom notifier. +func NewWeComNotifier(config *WeComConfig, ns notifications.WebhookSender, t *template.Template) *WeComNotifier { return &WeComNotifier{ Base: NewBase(&models.AlertNotification{ - Uid: model.UID, - Name: model.Name, - Type: model.Type, - DisableResolveMessage: model.DisableResolveMessage, - Settings: model.Settings, + Uid: config.UID, + Name: config.Name, + Type: config.Type, + DisableResolveMessage: config.DisableResolveMessage, + Settings: config.Settings, }), - URL: url, + URL: config.URL, + Message: config.Message, log: log.New("alerting.notifier.wecom"), ns: ns, - Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`), tmpl: t, - }, nil + } } // WeComNotifier is responsible for sending alert notifications to WeCom. diff --git a/pkg/services/ngalert/notifier/channels/wecom_test.go b/pkg/services/ngalert/notifier/channels/wecom_test.go index 77b045107b9..4c7bfd2b49f 100644 --- a/pkg/services/ngalert/notifier/channels/wecom_test.go +++ b/pkg/services/ngalert/notifier/channels/wecom_test.go @@ -79,7 +79,7 @@ func TestWeComNotifier(t *testing.T) { }, { name: "Error in initing", settings: `{}`, - expInitError: `failed to validate receiver "wecom_testing" of type "wecom": could not find webhook URL in settings`, + expInitError: `could not find webhook URL in settings`, }, } @@ -97,7 +97,7 @@ func TestWeComNotifier(t *testing.T) { webhookSender := mockNotificationService() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) decryptFn := secretsService.GetDecryptedValue - pn, err := NewWeComNotifier(m, webhookSender, tmpl, decryptFn) + cfg, err := NewWeComConfig(m, decryptFn) if c.expInitError != "" { require.Equal(t, c.expInitError, err.Error()) return @@ -106,6 +106,7 @@ func TestWeComNotifier(t *testing.T) { ctx := notify.WithGroupKey(context.Background(), "alertname") ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + pn := NewWeComNotifier(cfg, webhookSender, tmpl) ok, err := pn.Notify(ctx, c.alerts...) if c.expMsgError != nil { require.False(t, ok) diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index 6244a0f0fa0..094f2222cde 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -490,46 +490,15 @@ func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUser } return fallback } - - switch gr.Type { - case "email": - _, err = channels.NewEmailNotifier(cfg, nil, nil) // Email notifier already has a default template. - case "pagerduty": - _, err = channels.NewPagerdutyNotifier(cfg, nil, nil, decryptFunc) - case "pushover": - _, err = channels.NewPushoverNotifier(cfg, nil, nil, decryptFunc) - case "slack": - _, err = channels.NewSlackNotifier(cfg, nil, decryptFunc) - case "telegram": - _, err = channels.NewTelegramNotifier(cfg, nil, nil, decryptFunc) - case "victorops": - _, err = channels.NewVictoropsNotifier(cfg, nil, nil) - case "teams": - _, err = channels.NewTeamsNotifier(cfg, nil, nil) - case "dingding": - _, err = channels.NewDingDingNotifier(cfg, nil, nil) - case "kafka": - _, err = channels.NewKafkaNotifier(cfg, nil, nil) - case "webhook": - _, err = channels.NewWebHookNotifier(cfg, nil, nil, decryptFunc) - case "sensugo": - _, err = channels.NewSensuGoNotifier(cfg, nil, nil, decryptFunc) - case "discord": - _, err = channels.NewDiscordNotifier(cfg, nil, nil) - case "googlechat": - _, err = channels.NewGoogleChatNotifier(cfg, nil, nil) - case "LINE": - _, err = channels.NewLineNotifier(cfg, nil, nil, decryptFunc) - case "threema": - _, err = channels.NewThreemaNotifier(cfg, nil, nil, decryptFunc) - case "opsgenie": - _, err = channels.NewOpsgenieNotifier(cfg, nil, nil, decryptFunc) - case "prometheus-alertmanager": - _, err = channels.NewAlertmanagerNotifier(cfg, nil, decryptFunc) - default: + receiverFactory, exists := channels.Factory(gr.Type) + if !exists { return fmt.Errorf("notifier %s is not supported", gr.Type) } - + factoryConfig, err := channels.NewFactoryConfig(cfg, nil, decryptFunc, nil) + if err != nil { + return err + } + _, err = receiverFactory(factoryConfig) if err != nil { return err } diff --git a/pkg/services/sqlstore/migrations/ualert/ualert_test.go b/pkg/services/sqlstore/migrations/ualert/ualert_test.go index 967ff3d1a65..b136e3c2f3a 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert_test.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert_test.go @@ -32,7 +32,7 @@ func Test_validateAlertmanagerConfig(t *testing.T) { SecureSettings: map[string]string{"url": invalidUri}, }, }, - err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q: parse %q: net/url: invalid control character in URL", invalidUri, invalidUri), + err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q", invalidUri), }, { name: "when a slack receiver has an invalid recipient - it should not error",