From abd1050f98938611e3c02c889ad01cc97effd4fa Mon Sep 17 00:00:00 2001 From: gotjosh Date: Fri, 22 Oct 2021 12:16:36 +0100 Subject: [PATCH] [8.2.x] Alerting: Validate contact point configuration during migration to Unified Alerting (#40717) (#40801) * Alerting: Validate contact point configuration during migration to Unified Alerting (#40717) * Alerting: Validate contact point configuration during the migration This minimises the chances of generating broken configuration as part of the migration. Originally, we wanted to generate it and not produce a hard stop in Grafana but this strategy has the chance to avoid delivering notifications for our users. We now think it's better to hard stop the migration and let the user take care of resolving the configuration manually. (cherry picked from commit 74fb491b6af1e54e69dbfb8c88b476f449b29eed) --- .../ngalert/notifier/channels/alertmanager.go | 6 +- .../ngalert/notifier/channels/base.go | 32 +++++ .../ngalert/notifier/channels/dingding.go | 5 +- .../ngalert/notifier/channels/discord.go | 5 +- .../ngalert/notifier/channels/email.go | 5 +- .../ngalert/notifier/channels/googlechat.go | 5 +- .../ngalert/notifier/channels/kafka.go | 5 +- .../ngalert/notifier/channels/line.go | 11 +- .../ngalert/notifier/channels/opsgenie.go | 5 +- .../ngalert/notifier/channels/pagerduty.go | 13 +- .../ngalert/notifier/channels/pushover.go | 6 +- .../ngalert/notifier/channels/sensugo.go | 6 +- .../ngalert/notifier/channels/slack.go | 5 +- .../ngalert/notifier/channels/teams.go | 5 +- .../ngalert/notifier/channels/telegram.go | 11 +- .../ngalert/notifier/channels/threema.go | 13 +- .../ngalert/notifier/channels/victorops.go | 5 +- .../ngalert/notifier/channels/webhook.go | 13 +- .../sqlstore/migrations/ualert/channel.go | 16 --- .../migrations/ualert/channel_test.go | 120 ------------------ .../sqlstore/migrations/ualert/testing.go | 23 ++++ .../sqlstore/migrations/ualert/ualert.go | 113 +++++++++++++++-- .../sqlstore/migrations/ualert/ualert_test.go | 90 +++++++++++++ 23 files changed, 299 insertions(+), 219 deletions(-) create mode 100644 pkg/services/ngalert/notifier/channels/base.go delete mode 100644 pkg/services/sqlstore/migrations/ualert/channel_test.go create mode 100644 pkg/services/sqlstore/migrations/ualert/testing.go create mode 100644 pkg/services/sqlstore/migrations/ualert/ualert_test.go diff --git a/pkg/services/ngalert/notifier/channels/alertmanager.go b/pkg/services/ngalert/notifier/channels/alertmanager.go index 6db58ef5abf..a4a86bdfdd5 100644 --- a/pkg/services/ngalert/notifier/channels/alertmanager.go +++ b/pkg/services/ngalert/notifier/channels/alertmanager.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" + "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" ) @@ -44,7 +44,7 @@ func NewAlertmanagerNotifier(model *NotificationChannelConfig, t *template.Templ basicAuthPassword := model.DecryptedValue("basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString()) return &AlertmanagerNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, DisableResolveMessage: model.DisableResolveMessage, @@ -59,7 +59,7 @@ func NewAlertmanagerNotifier(model *NotificationChannelConfig, t *template.Templ // AlertmanagerNotifier sends alert notifications to the alert manager type AlertmanagerNotifier struct { - old_notifiers.NotifierBase + *Base urls []*url.URL basicAuthUser string diff --git a/pkg/services/ngalert/notifier/channels/base.go b/pkg/services/ngalert/notifier/channels/base.go new file mode 100644 index 00000000000..6f261bc7ef0 --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/base.go @@ -0,0 +1,32 @@ +package channels + +import ( + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" +) + +// Base is the base implementation of a notifier. It contains the common fields across all notifier types. +type Base struct { + Name string + Type string + UID string + IsDefault bool + DisableResolveMessage bool + + log log.Logger +} + +func (n *Base) GetDisableResolveMessage() bool { + return n.DisableResolveMessage +} + +func NewBase(model *models.AlertNotification) *Base { + return &Base{ + UID: model.Uid, + Name: model.Name, + IsDefault: model.IsDefault, + Type: model.Type, + DisableResolveMessage: model.DisableResolveMessage, + log: log.New("alerting.notifier." + model.Name), + } +} diff --git a/pkg/services/ngalert/notifier/channels/dingding.go b/pkg/services/ngalert/notifier/channels/dingding.go index 9f06f0ec56c..e6ac8d5f895 100644 --- a/pkg/services/ngalert/notifier/channels/dingding.go +++ b/pkg/services/ngalert/notifier/channels/dingding.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" ) const defaultDingdingMsgType = "link" @@ -31,7 +30,7 @@ func NewDingDingNotifier(model *NotificationChannelConfig, t *template.Template) msgType := model.Settings.Get("msgType").MustString(defaultDingdingMsgType) return &DingDingNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, @@ -48,7 +47,7 @@ func NewDingDingNotifier(model *NotificationChannelConfig, t *template.Template) // DingDingNotifier is responsible for sending alert notifications to ding ding. type DingDingNotifier struct { - old_notifiers.NotifierBase + *Base MsgType string URL string Message string diff --git a/pkg/services/ngalert/notifier/channels/discord.go b/pkg/services/ngalert/notifier/channels/discord.go index 0c086a96f71..faa1b53093a 100644 --- a/pkg/services/ngalert/notifier/channels/discord.go +++ b/pkg/services/ngalert/notifier/channels/discord.go @@ -13,12 +13,11 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" ) type DiscordNotifier struct { - old_notifiers.NotifierBase + *Base log log.Logger tmpl *template.Template Content string @@ -41,7 +40,7 @@ func NewDiscordNotifier(model *NotificationChannelConfig, t *template.Template) content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`) return &DiscordNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index 5deb62ea829..574f3d2ff91 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -11,14 +11,13 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/util" ) // EmailNotifier is responsible for sending // alert notifications over email. type EmailNotifier struct { - old_notifiers.NotifierBase + *Base Addresses []string SingleEmail bool Message string @@ -44,7 +43,7 @@ func NewEmailNotifier(model *NotificationChannelConfig, t *template.Template) (* addresses := util.SplitEmails(addressesString) return &EmailNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/googlechat.go b/pkg/services/ngalert/notifier/channels/googlechat.go index 56b30caf295..dcfd6bb45c0 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat.go +++ b/pkg/services/ngalert/notifier/channels/googlechat.go @@ -12,14 +12,13 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" ) // GoogleChatNotifier is responsible for sending // alert notifications to Google chat. type GoogleChatNotifier struct { - old_notifiers.NotifierBase + *Base URL string log log.Logger tmpl *template.Template @@ -32,7 +31,7 @@ func NewGoogleChatNotifier(model *NotificationChannelConfig, t *template.Templat } return &GoogleChatNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/kafka.go b/pkg/services/ngalert/notifier/channels/kafka.go index d9bad9e777d..bcfd9d052c0 100644 --- a/pkg/services/ngalert/notifier/channels/kafka.go +++ b/pkg/services/ngalert/notifier/channels/kafka.go @@ -13,13 +13,12 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" ) // KafkaNotifier is responsible for sending // alert notifications to Kafka. type KafkaNotifier struct { - old_notifiers.NotifierBase + *Base Endpoint string Topic string log log.Logger @@ -38,7 +37,7 @@ func NewKafkaNotifier(model *NotificationChannelConfig, t *template.Template) (* } return &KafkaNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/line.go b/pkg/services/ngalert/notifier/channels/line.go index 509d9a3c8bf..4e4dc2d60b8 100644 --- a/pkg/services/ngalert/notifier/channels/line.go +++ b/pkg/services/ngalert/notifier/channels/line.go @@ -6,13 +6,12 @@ import ( "net/url" "path" - "github.com/prometheus/alertmanager/template" - "github.com/prometheus/alertmanager/types" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" + + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" ) var ( @@ -27,7 +26,7 @@ func NewLineNotifier(model *NotificationChannelConfig, t *template.Template) (*L } return &LineNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, @@ -43,7 +42,7 @@ func NewLineNotifier(model *NotificationChannelConfig, t *template.Template) (*L // LineNotifier is responsible for sending // alert notifications to LINE. type LineNotifier struct { - old_notifiers.NotifierBase + *Base Token string log log.Logger tmpl *template.Template diff --git a/pkg/services/ngalert/notifier/channels/opsgenie.go b/pkg/services/ngalert/notifier/channels/opsgenie.go index d6f25a47da0..228a436f608 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" ) const ( @@ -32,7 +31,7 @@ var ( // OpsgenieNotifier is responsible for sending alert notifications to Opsgenie. type OpsgenieNotifier struct { - old_notifiers.NotifierBase + *Base APIKey string APIUrl string AutoClose bool @@ -63,7 +62,7 @@ func NewOpsgenieNotifier(model *NotificationChannelConfig, t *template.Template) } return &OpsgenieNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index 82871ab166c..ac718f7fd70 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -6,15 +6,14 @@ import ( "fmt" "os" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" ) const ( @@ -29,7 +28,7 @@ var ( // PagerdutyNotifier is responsible for sending // alert notifications to pagerduty type PagerdutyNotifier struct { - old_notifiers.NotifierBase + *Base Key string Severity string CustomDetails map[string]string @@ -53,7 +52,7 @@ func NewPagerdutyNotifier(model *NotificationChannelConfig, t *template.Template } return &PagerdutyNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/pushover.go b/pkg/services/ngalert/notifier/channels/pushover.go index 4eefd078663..cf50d9d9d85 100644 --- a/pkg/services/ngalert/notifier/channels/pushover.go +++ b/pkg/services/ngalert/notifier/channels/pushover.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" + "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" @@ -23,7 +23,7 @@ var ( // PushoverNotifier is responsible for sending // alert notifications to Pushover type PushoverNotifier struct { - old_notifiers.NotifierBase + *Base UserKey string APIToken string AlertingPriority int @@ -69,7 +69,7 @@ func NewPushoverNotifier(model *NotificationChannelConfig, t *template.Template) return nil, receiverInitError{Cfg: *model, Reason: "API token not found"} } return &PushoverNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/sensugo.go b/pkg/services/ngalert/notifier/channels/sensugo.go index 07a92b3a17e..94fe76644f2 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo.go +++ b/pkg/services/ngalert/notifier/channels/sensugo.go @@ -10,14 +10,14 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" + "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) type SensuGoNotifier struct { - old_notifiers.NotifierBase + *Base log log.Logger tmpl *template.Template @@ -47,7 +47,7 @@ func NewSensuGoNotifier(model *NotificationChannelConfig, t *template.Template) } return &SensuGoNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index 5a9975bedce..ecf576bcb4f 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -20,14 +20,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" ) // SlackNotifier is responsible for sending // alert notification to Slack. type SlackNotifier struct { - old_notifiers.NotifierBase + *Base log log.Logger tmpl *template.Template @@ -107,7 +106,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template) (* } return &SlackNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index 09599576746..2dfa4927851 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -11,13 +11,12 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" ) // TeamsNotifier is responsible for sending // alert notifications to Microsoft teams. type TeamsNotifier struct { - old_notifiers.NotifierBase + *Base URL string Message string tmpl *template.Template @@ -36,7 +35,7 @@ func NewTeamsNotifier(model *NotificationChannelConfig, t *template.Template) (* } return &TeamsNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/telegram.go b/pkg/services/ngalert/notifier/channels/telegram.go index ec396a4efaa..b66014df71e 100644 --- a/pkg/services/ngalert/notifier/channels/telegram.go +++ b/pkg/services/ngalert/notifier/channels/telegram.go @@ -6,13 +6,12 @@ import ( "fmt" "mime/multipart" - "github.com/prometheus/alertmanager/template" - "github.com/prometheus/alertmanager/types" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" + + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" ) var ( @@ -22,7 +21,7 @@ var ( // TelegramNotifier is responsible for sending // alert notifications to Telegram. type TelegramNotifier struct { - old_notifiers.NotifierBase + *Base BotToken string ChatID string Message string @@ -49,7 +48,7 @@ func NewTelegramNotifier(model *NotificationChannelConfig, t *template.Template) } return &TelegramNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/threema.go b/pkg/services/ngalert/notifier/channels/threema.go index ae67502da3f..322552407b4 100644 --- a/pkg/services/ngalert/notifier/channels/threema.go +++ b/pkg/services/ngalert/notifier/channels/threema.go @@ -7,14 +7,13 @@ import ( "path" "strings" - "github.com/prometheus/alertmanager/template" - "github.com/prometheus/alertmanager/types" - "github.com/prometheus/common/model" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" + + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/prometheus/common/model" ) var ( @@ -24,7 +23,7 @@ var ( // ThreemaNotifier is responsible for sending // alert notifications to Threema. type ThreemaNotifier struct { - old_notifiers.NotifierBase + *Base GatewayID string RecipientID string APISecret string @@ -63,7 +62,7 @@ func NewThreemaNotifier(model *NotificationChannelConfig, t *template.Template) } return &ThreemaNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/ngalert/notifier/channels/victorops.go b/pkg/services/ngalert/notifier/channels/victorops.go index 4a1c74342fb..39d9dfaf9ad 100644 --- a/pkg/services/ngalert/notifier/channels/victorops.go +++ b/pkg/services/ngalert/notifier/channels/victorops.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" ) @@ -35,7 +34,7 @@ func NewVictoropsNotifier(model *NotificationChannelConfig, t *template.Template } return &VictoropsNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, @@ -53,7 +52,7 @@ func NewVictoropsNotifier(model *NotificationChannelConfig, t *template.Template // and handles notification process by formatting POST body according to // Victorops specifications (http://victorops.force.com/knowledgebase/articles/Integration/Alert-Ingestion-API-Documentation/) type VictoropsNotifier struct { - old_notifiers.NotifierBase + *Base URL string MessageType string log log.Logger diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go index 13c0d1367f1..6f2e63fc9e6 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -4,21 +4,20 @@ import ( "context" "encoding/json" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" ) // WebhookNotifier is responsible for sending // alert notifications as webhooks. type WebhookNotifier struct { - old_notifiers.NotifierBase + *Base URL string User string Password string @@ -39,7 +38,7 @@ func NewWebHookNotifier(model *NotificationChannelConfig, t *template.Template) return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"} } return &WebhookNotifier{ - NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{ + Base: NewBase(&models.AlertNotification{ Uid: model.UID, Name: model.Name, Type: model.Type, diff --git a/pkg/services/sqlstore/migrations/ualert/channel.go b/pkg/services/sqlstore/migrations/ualert/channel.go index 2e5fd1b02ec..d0226ec6e32 100644 --- a/pkg/services/sqlstore/migrations/ualert/channel.go +++ b/pkg/services/sqlstore/migrations/ualert/channel.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "net/url" "sort" "strings" @@ -131,21 +130,6 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, orgID int64, channelUid return err } - // Grafana accepts any type of string as a URL for the Slack notification channel. - // However, the Alertmanager will fail if provided with an invalid URL we have two options at this point: - // Either we fail the migration or remove the URL, we've chosen the latter and assume that the notification - // channel was broken to begin with. - if c.Type == "slack" { - u, ok := decryptedSecureSettings["url"] - if ok { - _, err := url.Parse(u) - if err != nil { - m.mg.Logger.Warn("slack notification channel had invalid URL, removing", "name", c.Name, "uid", c.Uid, "org", c.OrgID) - delete(decryptedSecureSettings, "url") - } - } - } - portedChannels = append(portedChannels, &PostableGrafanaReceiver{ UID: uid, Name: c.Name, diff --git a/pkg/services/sqlstore/migrations/ualert/channel_test.go b/pkg/services/sqlstore/migrations/ualert/channel_test.go deleted file mode 100644 index 483a28bc43b..00000000000 --- a/pkg/services/sqlstore/migrations/ualert/channel_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package ualert - -import ( - "fmt" - "math/rand" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/components/securejsondata" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/util" -) - -func Test_makeReceiverAndRoute(t *testing.T) { - emptyMigration := func() *migration { - return &migration{ - mg: &migrator.Migrator{ - Logger: log.New("test"), - }, - migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}), - portedChannelGroupsPerOrg: make(map[int64]map[string]string), - seenChannelUIDs: make(map[string]struct{}), - } - } - - generateChannel := func(channelType string, settings map[string]interface{}, secureSettings map[string]string) *notificationChannel { - uid := util.GenerateShortUID() - return ¬ificationChannel{ - ID: rand.Int63(), - OrgID: rand.Int63(), - Uid: uid, - Name: fmt.Sprintf("Test-%s", uid), - Type: channelType, - DisableResolveMessage: rand.Int63()%2 == 0, - IsDefault: rand.Int63()%2 == 0, - Settings: simplejson.NewFromAny(settings), - SecureSettings: securejsondata.GetEncryptedJsonData(secureSettings), - } - } - - t.Run("Slack channel is migrated", func(t *testing.T) { - t.Run("url is removed if it is invalid (secure settings)", func(t *testing.T) { - secureSettings := map[string]string{ - "url": invalidUri, - "token": util.GenerateShortUID(), - } - settings := map[string]interface{}{ - "test": "data", - "some_map": map[string]interface{}{ - "test": rand.Int63(), - }, - } - - channel := generateChannel("slack", settings, secureSettings) - channelsUid := []interface{}{ - channel.Uid, - } - defaultChannels := make([]*notificationChannel, 0) - allChannels := map[interface{}]*notificationChannel{ - channel.Uid: channel, - } - - apiReceiver, _, err := emptyMigration().makeReceiverAndRoute(util.GenerateShortUID(), channel.OrgID, channelsUid, defaultChannels, allChannels) - require.NoError(t, err) - - require.Len(t, apiReceiver.GrafanaManagedReceivers, 1) - - receiver := apiReceiver.GrafanaManagedReceivers[0] - - require.NotContains(t, receiver.SecureSettings, "url") - require.Contains(t, receiver.SecureSettings, "token") - require.Equal(t, secureSettings["token"], receiver.SecureSettings["token"]) - actualSettings, err := receiver.Settings.Map() - require.NoError(t, err) - require.Equal(t, settings, actualSettings) - }) - - t.Run("url is removed if it is invalid (settings)", func(t *testing.T) { - secureSettings := map[string]string{ - "token": util.GenerateShortUID(), - } - settings := map[string]interface{}{ - "url": invalidUri, - "test": "data", - "some_map": map[string]interface{}{ - "test": rand.Int63(), - }, - } - - channel := generateChannel("slack", settings, secureSettings) - channelsUid := []interface{}{ - channel.Uid, - } - defaultChannels := make([]*notificationChannel, 0) - allChannels := map[interface{}]*notificationChannel{ - channel.Uid: channel, - } - - apiReceiver, _, err := emptyMigration().makeReceiverAndRoute(util.GenerateShortUID(), channel.OrgID, channelsUid, defaultChannels, allChannels) - require.NoError(t, err) - - require.Len(t, apiReceiver.GrafanaManagedReceivers, 1) - - receiver := apiReceiver.GrafanaManagedReceivers[0] - - require.NotContains(t, receiver.SecureSettings, "url") - require.Contains(t, receiver.SecureSettings, "token") - require.Equal(t, secureSettings["token"], receiver.SecureSettings["token"]) - actualSettings, err := receiver.Settings.Map() - require.NoError(t, err) - delete(settings, "url") - require.Equal(t, settings, actualSettings) - }) - }) -} - -const invalidUri = "�6�M��)uk譹1(�h`$�o�N>mĕ����cS2�dh![ę� ���`csB�!��OSxP�{�" diff --git a/pkg/services/sqlstore/migrations/ualert/testing.go b/pkg/services/sqlstore/migrations/ualert/testing.go new file mode 100644 index 00000000000..af99d06e665 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/testing.go @@ -0,0 +1,23 @@ +package ualert + +import ( + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +// newTestMigration generates an empty migration to use in tests. +func newTestMigration(t *testing.T) *migration { + t.Helper() + + return &migration{ + mg: &migrator.Migrator{ + + Logger: log.New("test"), + }, + migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}), + portedChannelGroupsPerOrg: make(map[int64]map[string]string), + seenChannelUIDs: make(map[string]struct{}), + } +} diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index c1a248d48f1..22e9ac63c59 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -1,6 +1,7 @@ package ualert import ( + "encoding/base64" "encoding/json" "fmt" "os" @@ -8,10 +9,11 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + pb "github.com/prometheus/alertmanager/silence/silencepb" "xorm.io/xorm" - - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) const GENERAL_FOLDER = "General Alerting" @@ -215,6 +217,7 @@ func (m *migration) SQL(dialect migrator.Dialect) string { return "code migration" } +//nolint: gocyclo func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { m.sess = sess m.mg = mg @@ -375,7 +378,24 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { return err } - if err := m.writeAlertmanagerConfig(orgID, amConfig, allChannelsPerOrg[orgID]); err != nil { + // No channels, hence don't require Alertmanager config - skip it. + if len(allChannelsPerOrg[orgID]) == 0 { + m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config") + continue + } + + // Encrypt the secure settings before we continue. + if err := amConfig.EncryptSecureSettings(); err != nil { + return err + } + + // Validate the alertmanager configuration produced, this gives a chance to catch bad configuration at migration time. + // Validation between legacy and unified alerting can be different (e.g. due to bug fixes) so this would fail the migration in that case. + if err := m.validateAlertmanagerConfig(orgID, amConfig); err != nil { + return err + } + + if err := m.writeAlertmanagerConfig(orgID, amConfig); err != nil { return err } @@ -387,22 +407,13 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { return nil } -func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel) error { - if len(allChannels) == 0 { - // No channels, hence don't require Alertmanager config. - m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config") - return nil - } - - if err := amConfig.EncryptSecureSettings(); err != nil { - return err - } +func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserConfig) error { rawAmConfig, err := json.Marshal(amConfig) if err != nil { return err } - // TODO: should we apply the config here? Because Alertmanager can take upto 1 min to pick it up. + // We don't need to apply the configuration, given the multi org alertmanager will do an initial sync before the server is ready. _, err = m.sess.Insert(AlertConfiguration{ AlertmanagerConfiguration: string(rawAmConfig), // Since we are migration for a snapshot of the code, it is always going to migrate to @@ -417,6 +428,80 @@ func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserC return nil } +// validateAlertmanagerConfig validates the alertmanager configuration produced by the migration against the receivers. +func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUserConfig) error { + for _, r := range config.AlertmanagerConfig.Receivers { + for _, gr := range r.GrafanaManagedReceivers { + // First, let's decode the secure settings - given they're stored as base64. + secureSettings := make(map[string][]byte, len(gr.SecureSettings)) + for k, v := range gr.SecureSettings { + d, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return err + } + secureSettings[k] = d + } + + var ( + cfg = &channels.NotificationChannelConfig{ + UID: gr.UID, + Name: gr.Name, + Type: gr.Type, + DisableResolveMessage: gr.DisableResolveMessage, + Settings: gr.Settings, + SecureSettings: secureSettings, + } + err error + ) + + switch gr.Type { + case "email": + _, err = channels.NewEmailNotifier(cfg, nil) // Email notifier already has a default template. + case "pagerduty": + _, err = channels.NewPagerdutyNotifier(cfg, nil) + case "pushover": + _, err = channels.NewPushoverNotifier(cfg, nil) + case "slack": + _, err = channels.NewSlackNotifier(cfg, nil) + case "telegram": + _, err = channels.NewTelegramNotifier(cfg, nil) + case "victorops": + _, err = channels.NewVictoropsNotifier(cfg, nil) + case "teams": + _, err = channels.NewTeamsNotifier(cfg, nil) + case "dingding": + _, err = channels.NewDingDingNotifier(cfg, nil) + case "kafka": + _, err = channels.NewKafkaNotifier(cfg, nil) + case "webhook": + _, err = channels.NewWebHookNotifier(cfg, nil) + case "sensugo": + _, err = channels.NewSensuGoNotifier(cfg, nil) + case "discord": + _, err = channels.NewDiscordNotifier(cfg, nil) + case "googlechat": + _, err = channels.NewGoogleChatNotifier(cfg, nil) + case "LINE": + _, err = channels.NewLineNotifier(cfg, nil) + case "threema": + _, err = channels.NewThreemaNotifier(cfg, nil) + case "opsgenie": + _, err = channels.NewOpsgenieNotifier(cfg, nil) + case "prometheus-alertmanager": + _, err = channels.NewAlertmanagerNotifier(cfg, nil) + default: + return fmt.Errorf("notifier %s is not supported", gr.Type) + } + + if err != nil { + return err + } + } + } + + return nil +} + type AlertConfiguration struct { ID int64 `xorm:"pk autoincr 'id'"` OrgID int64 `xorm:"org_id"` diff --git a/pkg/services/sqlstore/migrations/ualert/ualert_test.go b/pkg/services/sqlstore/migrations/ualert/ualert_test.go new file mode 100644 index 00000000000..3b9dd0ea165 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/ualert_test.go @@ -0,0 +1,90 @@ +package ualert + +import ( + "errors" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/util" + + "github.com/stretchr/testify/require" +) + +func Test_validateAlertmanagerConfig(t *testing.T) { + tc := []struct { + name string + receivers []*PostableGrafanaReceiver + err error + }{ + { + name: "when a slack receiver does not have a valid URL - it should error", + receivers: []*PostableGrafanaReceiver{ + { + UID: util.GenerateShortUID(), + Name: "SlackWithBadURL", + Type: "slack", + Settings: simplejson.NewFromAny(map[string]interface{}{}), + 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), + }, + { + name: "when a slack receiver has an invalid recipient - it should error", + receivers: []*PostableGrafanaReceiver{ + { + UID: util.GenerateShortUID(), + Name: "SlackWithBadRecipient", + Type: "slack", + Settings: simplejson.NewFromAny(map[string]interface{}{"recipient": "this-doesnt-pass"}), + SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"}, + }, + }, + err: errors.New("failed to validate receiver \"SlackWithBadRecipient\" of type \"slack\": recipient on invalid format: \"this-doesnt-pass\""), + }, + { + name: "when the configuration is valid - it should not error", + receivers: []*PostableGrafanaReceiver{ + { + UID: util.GenerateShortUID(), + Name: "SlackWithBadURL", + Type: "slack", + Settings: simplejson.NewFromAny(map[string]interface{}{"recipient": "#a-good-channel"}), + SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"}, + }, + }, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + mg := newTestMigration(t) + orgID := int64(1) + + config := configFromReceivers(t, tt.receivers) + require.NoError(t, config.EncryptSecureSettings()) // make sure we encrypt the settings + err := mg.validateAlertmanagerConfig(orgID, config) + if tt.err != nil { + require.Error(t, err) + require.EqualError(t, err, tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func configFromReceivers(t *testing.T, receivers []*PostableGrafanaReceiver) *PostableUserConfig { + t.Helper() + + return &PostableUserConfig{ + AlertmanagerConfig: PostableApiAlertingConfig{ + Receivers: []*PostableApiReceiver{ + {GrafanaManagedReceivers: receivers}, + }, + }, + } +} + +const invalidUri = "�6�M��)uk譹1(�h`$�o�N>mĕ����cS2�dh![ę� ���`csB�!��OSxP�{�"