From 0301d956daec4bcc4a23b6a144e23f840fc5ef0d Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Tue, 26 Apr 2022 10:17:30 -0400 Subject: [PATCH] Alerting: Create fewer contact points on migration (#47291) * Alerting: Create fewer contact points on migration Previously a new contact point was created for every unique combination of channels attached to any legacy alert. This was very hard to maintain, requiring modifications in every generated contact point. This change deduplicates the generated contact points to a more reasonable state. There should now only be one contact point per legacy channel, and we attached multiple contact points to a route by nesting them. The sole exception to this is if there were multiple default legacy channels, in which case we create a redundant contact point containing all of them used only in the root policy. This allows for a much simpler notification policy structure. Co-authored-by: gotjosh --- pkg/services/ngalert/CHANGELOG.md | 1 + .../sqlstore/migrations/ualert/channel.go | 453 +++++++++--------- .../migrations/ualert/channel_test.go | 349 ++++++++++++++ .../migrations/ualert/migration_test.go | 145 ++++-- .../sqlstore/migrations/ualert/testing.go | 4 +- .../sqlstore/migrations/ualert/ualert.go | 75 +-- .../sqlstore/migrations/ualert/ualert_test.go | 18 +- 7 files changed, 719 insertions(+), 326 deletions(-) create mode 100644 pkg/services/sqlstore/migrations/ualert/channel_test.go diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index a2f39f6b145..83569bee7ae 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -55,3 +55,4 @@ Scopes must have an order to ensure consistency and ease of search, this helps u - `grafana_alerting_ticker_last_consumed_tick_timestamp_seconds` - `grafana_alerting_ticker_next_tick_timestamp_seconds` - `grafana_alerting_ticker_interval_seconds` +- [ENHANCEMENT] Migration: Migrate each legacy notification channel to its own contact point, use nested routes to reproduce multi-channel alerts #47291 diff --git a/pkg/services/sqlstore/migrations/ualert/channel.go b/pkg/services/sqlstore/migrations/ualert/channel.go index 81c078c550e..27e0317c812 100644 --- a/pkg/services/sqlstore/migrations/ualert/channel.go +++ b/pkg/services/sqlstore/migrations/ualert/channel.go @@ -5,12 +5,11 @@ import ( "encoding/json" "errors" "fmt" - "sort" - "strings" + + "github.com/prometheus/alertmanager/pkg/labels" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/util" - "github.com/prometheus/alertmanager/pkg/labels" ) type notificationChannel struct { @@ -26,11 +25,88 @@ type notificationChannel struct { } // channelsPerOrg maps notification channels per organisation -type channelsPerOrg map[int64]map[interface{}]*notificationChannel +type channelsPerOrg map[int64][]*notificationChannel // channelMap maps notification channels per organisation type defaultChannelsPerOrg map[int64][]*notificationChannel +// uidOrID for both uid and ID, primarily used for mapping legacy channel to migrated receiver. +type uidOrID interface{} + +// setupAlertmanagerConfigs creates Alertmanager configs with migrated receivers and routes. +func (m *migration) setupAlertmanagerConfigs(rulesPerOrg map[int64]map[string]dashAlert) (amConfigsPerOrg, error) { + // allChannels: channelUID -> channelConfig + allChannelsPerOrg, defaultChannelsPerOrg, err := m.getNotificationChannelMap() + if err != nil { + return nil, fmt.Errorf("failed to load notification channels: %w", err) + } + + amConfigPerOrg := make(amConfigsPerOrg, len(allChannelsPerOrg)) + for orgID, channels := range allChannelsPerOrg { + amConfig := &PostableUserConfig{ + AlertmanagerConfig: PostableApiAlertingConfig{ + Receivers: make([]*PostableApiReceiver, 0), + }, + } + amConfigPerOrg[orgID] = amConfig + + // Create all newly migrated receivers from legacy notification channels. + receiversMap, receivers, err := m.createReceivers(channels) + if err != nil { + return nil, fmt.Errorf("failed to create receiver in orgId %d: %w", orgID, err) + } + + // No need to create an Alertmanager configuration if there are no receivers left that aren't obsolete. + if len(receivers) == 0 { + m.mg.Logger.Warn("no available receivers", "orgId", orgID) + continue + } + + amConfig.AlertmanagerConfig.Receivers = receivers + + defaultReceivers := make(map[string]struct{}) + defaultChannels, ok := defaultChannelsPerOrg[orgID] + if ok { + // If the organization has default channels build a map of default receivers, used to create alert-specific routes later. + for _, c := range defaultChannels { + defaultReceivers[c.Name] = struct{}{} + } + } + defaultReceiver, defaultRoute, err := m.createDefaultRouteAndReceiver(defaultChannels) + if err != nil { + return nil, fmt.Errorf("failed to create default route & receiver in orgId %d: %w", orgID, err) + } + amConfig.AlertmanagerConfig.Route = defaultRoute + if defaultReceiver != nil { + amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, defaultReceiver) + } + + // Create routes + if rules, ok := rulesPerOrg[orgID]; ok { + for ruleUid, da := range rules { + route, err := m.createRouteForAlert(ruleUid, da, receiversMap, defaultReceivers) + if err != nil { + return nil, fmt.Errorf("failed to create route for alert %s in orgId %d: %w", da.Name, orgID, err) + } + + if route != nil { + amConfigPerOrg[da.OrgId].AlertmanagerConfig.Route.Routes = append(amConfigPerOrg[da.OrgId].AlertmanagerConfig.Route.Routes, route) + } + } + } + + // 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 nil, fmt.Errorf("failed to validate AlertmanagerConfig in orgId %d: %w", orgID, err) + } + } + + return amConfigPerOrg, nil +} + +// getNotificationChannelMap returns a map of all channelUIDs to channel config as well as a separate map for just those channels that are default. +// For any given Organization, all channels in defaultChannelsPerOrg should also exist in channelsPerOrg. func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannelsPerOrg, error) { q := ` SELECT id, @@ -58,15 +134,13 @@ func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannels allChannelsMap := make(channelsPerOrg) defaultChannelsMap := make(defaultChannelsPerOrg) for i, c := range allChannels { - if _, ok := allChannelsMap[c.OrgID]; !ok { // new seen org - allChannelsMap[c.OrgID] = make(map[interface{}]*notificationChannel) - } - if c.Uid != "" { - allChannelsMap[c.OrgID][c.Uid] = &allChannels[i] - } - if c.ID != 0 { - allChannelsMap[c.OrgID][c.ID] = &allChannels[i] + if c.Type == "hipchat" || c.Type == "sensu" { + m.mg.Logger.Error("alert migration error: discontinued notification channel found", "type", c.Type, "name", c.Name, "uid", c.Uid) + continue } + + allChannelsMap[c.OrgID] = append(allChannelsMap[c.OrgID], &allChannels[i]) + if c.IsDefault { defaultChannelsMap[c.OrgID] = append(defaultChannelsMap[c.OrgID], &allChannels[i]) } @@ -75,257 +149,200 @@ func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannels return allChannelsMap, defaultChannelsMap, nil } -func (m *migration) updateReceiverAndRoute(allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg, da dashAlert, rule *alertRule, amConfig *PostableUserConfig) error { - // Create receiver and route for this rule. - if allChannels == nil { - return nil - } - - channelIDs := extractChannelIDs(da) - if len(channelIDs) == 0 { - // If there are no channels associated, we skip adding any routes, - // receivers or labels to rules so that it goes through the default - // route. - return nil - } - - recv, route, err := m.makeReceiverAndRoute(rule.UID, rule.OrgID, channelIDs, defaultChannels[rule.OrgID], allChannels[rule.OrgID]) +// Create a notifier (PostableGrafanaReceiver) from a legacy notification channel +func (m *migration) createNotifier(c *notificationChannel) (*PostableGrafanaReceiver, error) { + uid, err := m.generateChannelUID() if err != nil { - return err + return nil, err } - if recv != nil { - amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, recv) - } - if route != nil { - amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, route) + settings, secureSettings, err := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings) + if err != nil { + return nil, err } - return nil + return &PostableGrafanaReceiver{ + UID: uid, + Name: c.Name, + Type: c.Type, + DisableResolveMessage: c.DisableResolveMessage, + Settings: settings, + SecureSettings: secureSettings, + }, nil } -func (m *migration) makeReceiverAndRoute(ruleUid string, orgID int64, channelUids []interface{}, defaultChannels []*notificationChannel, allChannels map[interface{}]*notificationChannel) (*PostableApiReceiver, *Route, error) { - portedChannels := []*PostableGrafanaReceiver{} - var receiver *PostableApiReceiver - - addChannel := func(c *notificationChannel) error { - if c.Type == "hipchat" || c.Type == "sensu" { - m.mg.Logger.Error("alert migration error: discontinued notification channel found", "type", c.Type, "name", c.Name, "uid", c.Uid) - return nil - } - - uid, ok := m.generateChannelUID() - if !ok { - return errors.New("failed to generate UID for notification channel") - } - - if _, ok := m.migratedChannelsPerOrg[orgID]; !ok { - m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{}) - } - m.migratedChannelsPerOrg[orgID][c] = struct{}{} - settings, decryptedSecureSettings, err := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings) +// Create one receiver for every unique notification channel. +func (m *migration) createReceivers(allChannels []*notificationChannel) (map[uidOrID]*PostableApiReceiver, []*PostableApiReceiver, error) { + var receivers []*PostableApiReceiver + receiversMap := make(map[uidOrID]*PostableApiReceiver) + for _, c := range allChannels { + notifier, err := m.createNotifier(c) if err != nil { - return err + return nil, nil, err } - portedChannels = append(portedChannels, &PostableGrafanaReceiver{ - UID: uid, - Name: c.Name, - Type: c.Type, - DisableResolveMessage: c.DisableResolveMessage, - Settings: settings, - SecureSettings: decryptedSecureSettings, - }) + recv := &PostableApiReceiver{ + Name: c.Name, // Channel name is unique within an Org. + GrafanaManagedReceivers: []*PostableGrafanaReceiver{notifier}, + } - return nil - } + receivers = append(receivers, recv) - // Remove obsolete notification channels. - filteredChannelUids := make(map[interface{}]struct{}) - for _, uid := range channelUids { - c, ok := allChannels[uid] - if ok { - // always store the channel UID to prevent duplicates - filteredChannelUids[c.Uid] = struct{}{} - } else { - m.mg.Logger.Warn("ignoring obsolete notification channel", "uid", uid) - } - } - // Add default channels that are not obsolete. - for _, c := range defaultChannels { - id := interface{}(c.Uid) - if c.Uid == "" { - id = c.ID + // Store receivers for creating routes from alert rules later. + if c.Uid != "" { + receiversMap[c.Uid] = recv } - c, ok := allChannels[id] - if ok { - // always store the channel UID to prevent duplicates - filteredChannelUids[c.Uid] = struct{}{} + if c.ID != 0 { + // In certain circumstances, the alert rule uses ID instead of uid. So, we add this to be able to lookup by ID in case. + receiversMap[c.ID] = recv } } - if len(filteredChannelUids) == 0 && ruleUid != "default_route" { - // We use the default route instead. No need to add additional route. - return nil, nil, nil - } - - chanKey, err := makeKeyForChannelGroup(filteredChannelUids) - if err != nil { - return nil, nil, err - } - - var receiverName string + return receiversMap, receivers, nil +} - if _, ok := m.portedChannelGroupsPerOrg[orgID]; !ok { - m.portedChannelGroupsPerOrg[orgID] = make(map[string]string) - } - if rn, ok := m.portedChannelGroupsPerOrg[orgID][chanKey]; ok { - // We have ported these exact set of channels already. Re-use it. - receiverName = rn - if receiverName == "autogen-contact-point-default" { - // We don't need to create new routes if it's the default contact point. - return nil, nil, nil +// Create the root-level route with the default receiver. If no new receiver is created specifically for the root-level route, the returned receiver will be nil. +func (m *migration) createDefaultRouteAndReceiver(defaultChannels []*notificationChannel) (*PostableApiReceiver, *Route, error) { + var defaultReceiver *PostableApiReceiver + + defaultReceiverName := "autogen-contact-point-default" + if len(defaultChannels) != 1 { + // If there are zero or more than one default channels we create a separate contact group that is used only in the root policy. This is to simplify the migrated notification policy structure. + // If we ever allow more than one receiver per route this won't be necessary. + defaultReceiver = &PostableApiReceiver{ + Name: defaultReceiverName, + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, } - } else { - for n := range filteredChannelUids { - if err := addChannel(allChannels[n]); err != nil { + + for _, c := range defaultChannels { + // Need to create a new notifier to prevent uid conflict. + defaultNotifier, err := m.createNotifier(c) + if err != nil { return nil, nil, err } - } - - if ruleUid == "default_route" { - receiverName = "autogen-contact-point-default" - } else { - m.lastReceiverID++ - receiverName = fmt.Sprintf("autogen-contact-point-%d", m.lastReceiverID) - } - m.portedChannelGroupsPerOrg[orgID][chanKey] = receiverName - receiver = &PostableApiReceiver{ - Name: receiverName, - GrafanaManagedReceivers: portedChannels, + defaultReceiver.GrafanaManagedReceivers = append(defaultReceiver.GrafanaManagedReceivers, defaultNotifier) } + } else { + // If there is only a single default channel, we don't need a separate receiver to hold it. We can reuse the existing receiver for that single notifier. + defaultReceiverName = defaultChannels[0].Name } - n, v := getLabelForRouteMatching(ruleUid) - mat, err := labels.NewMatcher(labels.MatchEqual, n, v) - if err != nil { - return nil, nil, err - } - route := &Route{ - Receiver: receiverName, - Matchers: Matchers{mat}, + defaultRoute := &Route{ + Receiver: defaultReceiverName, + Routes: make([]*Route, 0), } - return receiver, route, nil + return defaultReceiver, defaultRoute, nil } -// makeKeyForChannelGroup generates a unique for this group of channels UIDs. -func makeKeyForChannelGroup(channelUids map[interface{}]struct{}) (string, error) { - uids := make([]string, 0, len(channelUids)) - for u := range channelUids { - switch uid := u.(type) { - case string: - uids = append(uids, uid) - case int, int32, int64: - uids = append(uids, fmt.Sprintf("%d", uid)) - default: - // Should never happen. - return "", fmt.Errorf("unknown channel UID type: %T", u) +// Wrapper to select receivers for given alert rules based on associated notification channels and then create the migrated route. +func (m *migration) createRouteForAlert(ruleUID string, da dashAlert, receivers map[uidOrID]*PostableApiReceiver, defaultReceivers map[string]struct{}) (*Route, error) { + // Create route(s) for alert + filteredReceiverNames := m.filterReceiversForAlert(da, receivers, defaultReceivers) + + if len(filteredReceiverNames) != 0 { + // Only create a route if there are specific receivers, otherwise it defaults to the root-level route. + route, err := createRoute(ruleUID, filteredReceiverNames) + if err != nil { + return nil, err } + + return route, nil } - sort.Strings(uids) - return strings.Join(uids, "::sep::"), nil + return nil, nil } -// addDefaultChannels should be called before adding any other routes. -func (m *migration) addDefaultChannels(amConfigsPerOrg amConfigsPerOrg, allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg) error { - for orgID := range allChannels { - if _, ok := amConfigsPerOrg[orgID]; !ok { - amConfigsPerOrg[orgID] = &PostableUserConfig{ - AlertmanagerConfig: PostableApiAlertingConfig{ - Receivers: make([]*PostableApiReceiver, 0), - Route: &Route{ - Routes: make([]*Route, 0), - }, - }, +// Create route(s) for the given alert ruleUID and receivers. +// If the alert had a single channel, it will now have a single route/policy. If the alert had multiple channels, it will now have multiple nested routes/policies. +func createRoute(ruleUID string, filteredReceiverNames map[string]interface{}) (*Route, error) { + n, v := getLabelForRouteMatching(ruleUID) + mat, err := labels.NewMatcher(labels.MatchEqual, n, v) + if err != nil { + return nil, err + } + + var route *Route + if len(filteredReceiverNames) == 1 { + for name := range filteredReceiverNames { + route = &Route{ + Receiver: name, + Matchers: Matchers{mat}, } } - // Default route and receiver. - recv, route, err := m.makeReceiverAndRoute("default_route", orgID, nil, defaultChannels[orgID], allChannels[orgID]) - if err != nil { - // if one fails it will fail the migration - return err + } else { + nestedRoutes := []*Route{} + for name := range filteredReceiverNames { + r := &Route{ + Receiver: name, + Matchers: Matchers{mat}, + Continue: true, + } + nestedRoutes = append(nestedRoutes, r) } - if recv != nil { - amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers = append(amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers, recv) - } - if route != nil { - route.Matchers = nil // Don't need matchers for root route. - amConfigsPerOrg[orgID].AlertmanagerConfig.Route = route + route = &Route{ + Matchers: Matchers{mat}, + Routes: nestedRoutes, } } - return nil + + return route, nil } -func (m *migration) addUnmigratedChannels(orgID int64, amConfigs *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error { - // Unmigrated channels. - portedChannels := []*PostableGrafanaReceiver{} - receiver := &PostableApiReceiver{ - Name: "autogen-unlinked-channel-recv", +// Filter receivers to select those that were associated to the given rule as channels. +func (m *migration) filterReceiversForAlert(da dashAlert, receivers map[uidOrID]*PostableApiReceiver, defaultReceivers map[string]struct{}) map[string]interface{} { + channelIDs := extractChannelIDs(da) + if len(channelIDs) == 0 { + // If there are no channels associated, we use the default route. + return nil } - for _, c := range allChannels { - if _, ok := m.migratedChannelsPerOrg[orgID]; !ok { - m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{}) - } - _, ok := m.migratedChannelsPerOrg[orgID][c] + + // Filter receiver names. + filteredReceiverNames := make(map[string]interface{}) + for _, uidOrId := range channelIDs { + recv, ok := receivers[uidOrId] if ok { - continue - } - if c.Type == "hipchat" || c.Type == "sensu" { - m.mg.Logger.Error("alert migration error: discontinued notification channel found", "type", c.Type, "name", c.Name, "uid", c.Uid) - continue + filteredReceiverNames[recv.Name] = struct{}{} // Deduplicate on contact point name. + } else { + m.mg.Logger.Warn("alert linked to obsolete notification channel, ignoring", "alert", da.Name, "uid", uidOrId) } + } - uid, ok := m.generateChannelUID() - if !ok { - return errors.New("failed to generate UID for notification channel") + coveredByDefault := func(names map[string]interface{}) bool { + // Check if all receivers are also default ones and if so, just use the default route. + for n := range names { + if _, ok := defaultReceivers[n]; !ok { + return false + } } + return true + } - m.migratedChannelsPerOrg[orgID][c] = struct{}{} - settings, decryptedSecureSettings, err := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings) - if err != nil { - return err - } - portedChannels = append(portedChannels, &PostableGrafanaReceiver{ - UID: uid, - Name: c.Name, - Type: c.Type, - DisableResolveMessage: c.DisableResolveMessage, - Settings: settings, - SecureSettings: decryptedSecureSettings, - }) + if len(filteredReceiverNames) == 0 || coveredByDefault(filteredReceiverNames) { + // Use the default route instead. + return nil } - receiver.GrafanaManagedReceivers = portedChannels - if len(portedChannels) > 0 { - amConfigs.AlertmanagerConfig.Receivers = append(amConfigs.AlertmanagerConfig.Receivers, receiver) + + // Add default receivers alongside rule-specific ones. + for n := range defaultReceivers { + filteredReceiverNames[n] = struct{}{} } - return nil + return filteredReceiverNames } -func (m *migration) generateChannelUID() (string, bool) { +func (m *migration) generateChannelUID() (string, error) { for i := 0; i < 5; i++ { gen := util.GenerateShortUID() if _, ok := m.seenChannelUIDs[gen]; !ok { m.seenChannelUIDs[gen] = struct{}{} - return gen, true + return gen, nil } } - return "", false + return "", errors.New("failed to generate UID for notification channel") } // Some settings were migrated from settings to secure settings in between. @@ -354,7 +371,7 @@ func migrateSettingsToSecureSettings(chanType string, settings *simplejson.Json, keys = []string{"api_secret"} } - decryptedSecureSettings := secureSettings.Decrypt() + newSecureSettings := secureSettings.Decrypt() cloneSettings := simplejson.New() settingsMap, err := settings.Map() if err != nil { @@ -364,25 +381,30 @@ func migrateSettingsToSecureSettings(chanType string, settings *simplejson.Json, cloneSettings.Set(k, v) } for _, k := range keys { - if v, ok := decryptedSecureSettings[k]; ok && v != "" { + if v, ok := newSecureSettings[k]; ok && v != "" { continue } sv := cloneSettings.Get(k).MustString() if sv != "" { - decryptedSecureSettings[k] = sv + newSecureSettings[k] = sv cloneSettings.Del(k) } } - return cloneSettings, decryptedSecureSettings, nil + encryptedData := GetEncryptedJsonData(newSecureSettings) + for k, v := range encryptedData { + newSecureSettings[k] = base64.StdEncoding.EncodeToString(v) + } + + return cloneSettings, newSecureSettings, nil } func getLabelForRouteMatching(ruleUID string) (string, string) { return "rule_uid", ruleUID } -func extractChannelIDs(d dashAlert) (channelUids []interface{}) { +func extractChannelIDs(d dashAlert) (channelUids []uidOrID) { // Extracting channel UID/ID. for _, ui := range d.ParsedSettings.Notifications { if ui.UID != "" { @@ -409,18 +431,6 @@ type PostableUserConfig struct { type amConfigsPerOrg = map[int64]*PostableUserConfig -func (c *PostableUserConfig) EncryptSecureSettings() error { - for _, r := range c.AlertmanagerConfig.Receivers { - for _, gr := range r.GrafanaManagedReceivers { - encryptedData := GetEncryptedJsonData(gr.SecureSettings) - for k, v := range encryptedData { - gr.SecureSettings[k] = base64.StdEncoding.EncodeToString(v) - } - } - } - return nil -} - type PostableApiAlertingConfig struct { Route *Route `yaml:"route,omitempty" json:"route,omitempty"` Templates []string `yaml:"templates" json:"templates"` @@ -431,6 +441,7 @@ type Route struct { Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` + Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` } type Matchers labels.Matchers diff --git a/pkg/services/sqlstore/migrations/ualert/channel_test.go b/pkg/services/sqlstore/migrations/ualert/channel_test.go new file mode 100644 index 00000000000..688b3d62eb1 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/channel_test.go @@ -0,0 +1,349 @@ +package ualert + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" +) + +func TestFilterReceiversForAlert(t *testing.T) { + tc := []struct { + name string + da dashAlert + receivers map[uidOrID]*PostableApiReceiver + defaultReceivers map[string]struct{} + expected map[string]interface{} + }{ + { + name: "when an alert has multiple channels, each should filter for the correct receiver", + da: dashAlert{ + ParsedSettings: &dashAlertSettings{ + Notifications: []dashAlertNot{{UID: "uid1"}, {UID: "uid2"}}, + }, + }, + receivers: map[uidOrID]*PostableApiReceiver{ + "uid1": { + Name: "recv1", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + "uid2": { + Name: "recv2", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + "uid3": { + Name: "recv3", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + }, + defaultReceivers: map[string]struct{}{}, + expected: map[string]interface{}{ + "recv1": struct{}{}, + "recv2": struct{}{}, + }, + }, + { + name: "when default receivers exist, they should be added to an alert's filtered receivers", + da: dashAlert{ + ParsedSettings: &dashAlertSettings{ + Notifications: []dashAlertNot{{UID: "uid1"}}, + }, + }, + receivers: map[uidOrID]*PostableApiReceiver{ + "uid1": { + Name: "recv1", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + "uid2": { + Name: "recv2", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + "uid3": { + Name: "recv3", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + }, + defaultReceivers: map[string]struct{}{ + "recv2": {}, + }, + expected: map[string]interface{}{ + "recv1": struct{}{}, // From alert + "recv2": struct{}{}, // From default + }, + }, + { + name: "when an alert has a channels associated by ID instead of UID, it should be included", + da: dashAlert{ + ParsedSettings: &dashAlertSettings{ + Notifications: []dashAlertNot{{ID: int64(42)}}, + }, + }, + receivers: map[uidOrID]*PostableApiReceiver{ + int64(42): { + Name: "recv1", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + }, + defaultReceivers: map[string]struct{}{}, + expected: map[string]interface{}{ + "recv1": struct{}{}, + }, + }, + { + name: "when an alert's receivers are covered by the defaults, return nil to use default receiver downstream", + da: dashAlert{ + ParsedSettings: &dashAlertSettings{ + Notifications: []dashAlertNot{{UID: "uid1"}}, + }, + }, + receivers: map[uidOrID]*PostableApiReceiver{ + "uid1": { + Name: "recv1", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + "uid2": { + Name: "recv2", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + "uid3": { + Name: "recv3", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + }, + defaultReceivers: map[string]struct{}{ + "recv1": {}, + "recv2": {}, + }, + expected: nil, // recv1 is already a default + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + m := newTestMigration(t) + res := m.filterReceiversForAlert(tt.da, tt.receivers, tt.defaultReceivers) + + require.Equal(t, tt.expected, res) + }) + } +} + +func TestCreateRoute(t *testing.T) { + tc := []struct { + name string + ruleUID string + filteredReceiverNames map[string]interface{} + expected *Route + expErr error + }{ + { + name: "when a single receiver is passed in, the route should be simple and not nested", + ruleUID: "r_uid1", + filteredReceiverNames: map[string]interface{}{ + "recv1": struct{}{}, + }, + expected: &Route{ + Receiver: "recv1", + Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, + Routes: nil, + Continue: false, + }, + }, + { + name: "when multiple receivers are passed in, the route should be nested with continue=true", + ruleUID: "r_uid1", + filteredReceiverNames: map[string]interface{}{ + "recv1": struct{}{}, + "recv2": struct{}{}, + }, + expected: &Route{ + Receiver: "", + Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, + Routes: []*Route{ + { + Receiver: "recv1", + Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, + Routes: nil, + Continue: true, + }, + { + Receiver: "recv2", + Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, + Routes: nil, + Continue: true, + }, + }, + Continue: false, + }, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + res, err := createRoute(tt.ruleUID, tt.filteredReceiverNames) + if tt.expErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.expErr.Error()) + return + } + + require.NoError(t, err) + + // Compare route slice separately since order is not guaranteed + expRoutes := tt.expected.Routes + tt.expected.Routes = nil + actRoutes := res.Routes + res.Routes = nil + + require.Equal(t, tt.expected, res) + require.ElementsMatch(t, expRoutes, actRoutes) + }) + } +} + +func createNotChannel(t *testing.T, uid string, id int64, name string) *notificationChannel { + t.Helper() + return ¬ificationChannel{Uid: uid, ID: id, Name: name, Settings: simplejson.New()} +} + +func TestCreateReceivers(t *testing.T) { + tc := []struct { + name string + allChannels []*notificationChannel + defaultChannels []*notificationChannel + expRecvMap map[uidOrID]*PostableApiReceiver + expRecv []*PostableApiReceiver + expErr error + }{ + { + name: "when given notification channels migrate them to receivers", + allChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1"), createNotChannel(t, "uid2", int64(2), "name2")}, + expRecvMap: map[uidOrID]*PostableApiReceiver{ + "uid1": { + Name: "name1", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}}, + }, + "uid2": { + Name: "name2", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}}, + }, + int64(1): { + Name: "name1", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}}, + }, + int64(2): { + Name: "name2", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}}, + }, + }, + expRecv: []*PostableApiReceiver{ + { + Name: "name1", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}}, + }, + { + Name: "name2", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}}, + }, + }, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + m := newTestMigration(t) + recvMap, recvs, err := m.createReceivers(tt.allChannels) + if tt.expErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.expErr.Error()) + return + } + + require.NoError(t, err) + + // We ignore certain fields for the purposes of this test + for _, recv := range recvs { + for _, not := range recv.GrafanaManagedReceivers { + not.UID = "" + not.Settings = nil + not.SecureSettings = nil + } + } + + require.Equal(t, tt.expRecvMap, recvMap) + require.ElementsMatch(t, tt.expRecv, recvs) + }) + } +} + +func TestCreateDefaultRouteAndReceiver(t *testing.T) { + tc := []struct { + name string + amConfig *PostableUserConfig + defaultChannels []*notificationChannel + expRecv *PostableApiReceiver + expRoute *Route + expErr error + }{ + { + name: "when given multiple default notification channels migrate them to a single receiver", + defaultChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1"), createNotChannel(t, "uid2", int64(2), "name2")}, + expRecv: &PostableApiReceiver{ + Name: "autogen-contact-point-default", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}, {Name: "name2"}}, + }, + expRoute: &Route{ + Receiver: "autogen-contact-point-default", + Routes: make([]*Route, 0), + }, + }, + { + name: "when given no default notification channels create a single empty receiver for default", + defaultChannels: []*notificationChannel{}, + expRecv: &PostableApiReceiver{ + Name: "autogen-contact-point-default", + GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, + }, + expRoute: &Route{ + Receiver: "autogen-contact-point-default", + Routes: make([]*Route, 0), + }, + }, + { + name: "when given a single default notification channels don't create a new default receiver", + defaultChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1")}, + expRecv: nil, + expRoute: &Route{ + Receiver: "name1", + Routes: make([]*Route, 0), + }, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + m := newTestMigration(t) + recv, route, err := m.createDefaultRouteAndReceiver(tt.defaultChannels) + if tt.expErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.expErr.Error()) + return + } + + require.NoError(t, err) + + // We ignore certain fields for the purposes of this test + if recv != nil { + for _, not := range recv.GrafanaManagedReceivers { + not.UID = "" + not.Settings = nil + not.SecureSettings = nil + } + } + + require.Equal(t, tt.expRecv, recv) + require.Equal(t, tt.expRoute, route) + }) + } +} diff --git a/pkg/services/sqlstore/migrations/ualert/migration_test.go b/pkg/services/sqlstore/migrations/ualert/migration_test.go index 8f7de0ef342..0061d9f5871 100644 --- a/pkg/services/sqlstore/migrations/ualert/migration_test.go +++ b/pkg/services/sqlstore/migrations/ualert/migration_test.go @@ -137,15 +137,18 @@ func TestDashAlertMigration(t *testing.T) { Route: &ualert.Route{ Receiver: "autogen-contact-point-default", Routes: []*ualert.Route{ - {Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert1")}, // These Matchers are temporary and will be replaced below with generated rule_uid. - {Receiver: "autogen-contact-point-2", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert2")}, - {Receiver: "autogen-contact-point-3", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert3")}, + {Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")}, // These Matchers are temporary and will be replaced below with generated rule_uid. + {Matchers: createAlertNameMatchers("alert2"), Routes: []*ualert.Route{ + {Receiver: "notifier2", Matchers: createAlertNameMatchers("alert2"), Continue: true}, + {Receiver: "notifier3", Matchers: createAlertNameMatchers("alert2"), Continue: true}, + }}, + {Receiver: "notifier3", Matchers: createAlertNameMatchers("alert3")}, }, }, Receivers: []*ualert.PostableApiReceiver{ - {Name: "autogen-contact-point-1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, // email - {Name: "autogen-contact-point-2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}, {Name: "notifier3", Type: "opsgenie"}}}, // slack+opsgenie - {Name: "autogen-contact-point-3", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}, // opsgenie + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}, + {Name: "notifier3", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}, {Name: "autogen-contact-point-default"}, // empty default }, }, @@ -153,29 +156,74 @@ func TestDashAlertMigration(t *testing.T) { int64(2): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "notifier6", Routes: []*ualert.Route{ - {Receiver: "autogen-contact-point-4", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert4")}, - {Receiver: "autogen-contact-point-5", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert5")}, + {Matchers: createAlertNameMatchers("alert4"), Routes: []*ualert.Route{ + {Receiver: "notifier4", Matchers: createAlertNameMatchers("alert4"), Continue: true}, + {Receiver: "notifier6", Matchers: createAlertNameMatchers("alert4"), Continue: true}, + }}, + {Matchers: createAlertNameMatchers("alert5"), Routes: []*ualert.Route{ + {Receiver: "notifier4", Matchers: createAlertNameMatchers("alert5"), Continue: true}, + {Receiver: "notifier5", Matchers: createAlertNameMatchers("alert5"), Continue: true}, + {Receiver: "notifier6", Matchers: createAlertNameMatchers("alert5"), Continue: true}, + }}, }, }, Receivers: []*ualert.PostableApiReceiver{ - {Name: "autogen-contact-point-4", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier4", Type: "email"}, {Name: "notifier6", Type: "opsgenie"}}}, // email - {Name: "autogen-contact-point-5", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier4", Type: "email"}, {Name: "notifier5", Type: "slack"}, {Name: "notifier6", Type: "opsgenie"}}}, // email+slack+opsgenie - {Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}}, // empty default + {Name: "notifier4", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier4", Type: "email"}}}, + {Name: "notifier5", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier5", Type: "slack"}}}, + {Name: "notifier6", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}}, // empty default }, }, }, }, }, { - name: "when default channel, add to autogen-contact-point-default", + name: "when no default channel, create empty autogen-contact-point-default", legacyChannels: []*models.AlertNotification{ - createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true), // default + createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false), }, - alerts: []*models.Alert{ - createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier1"}), + alerts: []*models.Alert{}, + expected: map[int64]*ualert.PostableUserConfig{ + int64(1): { + AlertmanagerConfig: ualert.PostableApiAlertingConfig{ + Route: &ualert.Route{ + Receiver: "autogen-contact-point-default", + }, + Receivers: []*ualert.PostableApiReceiver{ + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "autogen-contact-point-default"}, + }, + }, + }, + }, + }, + { + name: "when single default channel, don't create autogen-contact-point-default", + legacyChannels: []*models.AlertNotification{ + createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true), }, + alerts: []*models.Alert{}, + expected: map[int64]*ualert.PostableUserConfig{ + int64(1): { + AlertmanagerConfig: ualert.PostableApiAlertingConfig{ + Route: &ualert.Route{ + Receiver: "notifier1", + }, + Receivers: []*ualert.PostableApiReceiver{ + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + }, + }, + }, + }, + }, + { + name: "when multiple default channels, add them to autogen-contact-point-default as well", + legacyChannels: []*models.AlertNotification{ + createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true), + createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, true), + }, + alerts: []*models.Alert{}, expected: map[int64]*ualert.PostableUserConfig{ int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ @@ -183,7 +231,9 @@ func TestDashAlertMigration(t *testing.T) { Receiver: "autogen-contact-point-default", }, Receivers: []*ualert.PostableApiReceiver{ - {Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}, + {Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}}, }, }, }, @@ -196,22 +246,18 @@ func TestDashAlertMigration(t *testing.T) { createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false), createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, true), // default }, - alerts: []*models.Alert{ - createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier2"}), // + notifier1, notifier3 - }, + alerts: []*models.Alert{}, expected: map[int64]*ualert.PostableUserConfig{ int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ Receiver: "autogen-contact-point-default", - Routes: []*ualert.Route{ - {Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert1")}, - }, }, Receivers: []*ualert.PostableApiReceiver{ - {Name: "autogen-contact-point-1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}, {Name: "notifier3", Type: "opsgenie"}}}, - {Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier3", Type: "opsgenie"}}}, - }, + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}, + {Name: "notifier3", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}, + {Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier3", Type: "opsgenie"}}}}, }, }, }, @@ -233,6 +279,8 @@ func TestDashAlertMigration(t *testing.T) { Receiver: "autogen-contact-point-default", }, Receivers: []*ualert.PostableApiReceiver{ + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}, {Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}}, }, }, @@ -240,13 +288,13 @@ func TestDashAlertMigration(t *testing.T) { }, }, { - name: "when alerts share all channels, only create one receiver for all of them", + name: "when alerts share channels, only create one receiver per legacy channel", legacyChannels: []*models.AlertNotification{ createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false), createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false), }, alerts: []*models.Alert{ - createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier1", "notifier2"}), + createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier1"}), createAlert(t, int64(1), int64(1), int64(1), "alert2", []string{"notifier1", "notifier2"}), }, expected: map[int64]*ualert.PostableUserConfig{ @@ -255,12 +303,16 @@ func TestDashAlertMigration(t *testing.T) { Route: &ualert.Route{ Receiver: "autogen-contact-point-default", Routes: []*ualert.Route{ - {Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert1")}, - {Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert2")}, + {Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")}, + {Matchers: createAlertNameMatchers("alert2"), Routes: []*ualert.Route{ + {Receiver: "notifier1", Matchers: createAlertNameMatchers("alert2"), Continue: true}, + {Receiver: "notifier2", Matchers: createAlertNameMatchers("alert2"), Continue: true}, + }}, }, }, Receivers: []*ualert.PostableApiReceiver{ - {Name: "autogen-contact-point-1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}}, + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}, {Name: "autogen-contact-point-default"}, }, }, @@ -268,16 +320,11 @@ func TestDashAlertMigration(t *testing.T) { }, }, { - name: "when channel not linked to any alerts, migrate it to autogen-unlinked-channel-recv", + name: "when channel not linked to any alerts, still create a receiver for it", legacyChannels: []*models.AlertNotification{ - createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true), // default - createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, true), // default - createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, false), // unlinked - }, - alerts: []*models.Alert{ - createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier1"}), - createAlert(t, int64(1), int64(2), int64(3), "alert3", []string{}), + createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false), }, + alerts: []*models.Alert{}, expected: map[int64]*ualert.PostableUserConfig{ int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ @@ -285,8 +332,8 @@ func TestDashAlertMigration(t *testing.T) { Receiver: "autogen-contact-point-default", }, Receivers: []*ualert.PostableApiReceiver{ - {Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}}, - {Name: "autogen-unlinked-channel-recv", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}, + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, + {Name: "autogen-contact-point-default"}, }, }, }, @@ -295,8 +342,9 @@ func TestDashAlertMigration(t *testing.T) { { name: "when unsupported channels, do not migrate them", legacyChannels: []*models.AlertNotification{ - createAlertNotification(t, int64(1), "notifier1", "hipchat", "", false), - createAlertNotification(t, int64(1), "notifier2", "sensu", "", false), + createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false), + createAlertNotification(t, int64(1), "notifier2", "hipchat", "", false), + createAlertNotification(t, int64(1), "notifier3", "sensu", "", false), }, alerts: []*models.Alert{}, expected: map[int64]*ualert.PostableUserConfig{ @@ -306,6 +354,7 @@ func TestDashAlertMigration(t *testing.T) { Receiver: "autogen-contact-point-default", }, Receivers: []*ualert.PostableApiReceiver{ + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, {Name: "autogen-contact-point-default"}, }, }, @@ -327,11 +376,11 @@ func TestDashAlertMigration(t *testing.T) { Route: &ualert.Route{ Receiver: "autogen-contact-point-default", Routes: []*ualert.Route{ - {Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert1")}, + {Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")}, }, }, Receivers: []*ualert.PostableApiReceiver{ - {Name: "autogen-contact-point-1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, // no sensu + {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, {Name: "autogen-contact-point-default"}, }, }, @@ -592,8 +641,8 @@ func boolPointer(b bool) *bool { return &b } -// newMatchers creates a new ualert.Matchers given MatchType, name, and value. -func newMatchers(t labels.MatchType, n, v string) ualert.Matchers { - matcher, _ := labels.NewMatcher(t, n, v) +// createAlertNameMatchers creates a temporary alert_name Matchers that will be replaced during runtime with the generated rule_uid. +func createAlertNameMatchers(alertName string) ualert.Matchers { + matcher, _ := labels.NewMatcher(labels.MatchEqual, "alert_name", alertName) return ualert.Matchers(labels.Matchers{matcher}) } diff --git a/pkg/services/sqlstore/migrations/ualert/testing.go b/pkg/services/sqlstore/migrations/ualert/testing.go index af99d06e665..d238a7ce4f5 100644 --- a/pkg/services/sqlstore/migrations/ualert/testing.go +++ b/pkg/services/sqlstore/migrations/ualert/testing.go @@ -16,8 +16,6 @@ func newTestMigration(t *testing.T) *migration { Logger: log.New("test"), }, - migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}), - portedChannelGroupsPerOrg: make(map[int64]map[string]string), - seenChannelUIDs: make(map[string]struct{}), + seenChannelUIDs: make(map[string]struct{}), } } diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index 8078478d0dc..7c91f53aaf2 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -69,10 +69,8 @@ func AddDashAlertMigration(mg *migrator.Migrator) { mg.Logger.Error("alert migration error: could not clear alert migration for removing data", "error", err) } mg.AddMigration(migTitle, &migration{ - seenChannelUIDs: make(map[string]struct{}), - migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}), - portedChannelGroupsPerOrg: make(map[int64]map[string]string), - silences: make(map[int64][]*pb.MeshSilence), + seenChannelUIDs: make(map[string]struct{}), + silences: make(map[int64][]*pb.MeshSilence), }) case !mg.Cfg.UnifiedAlerting.IsEnabled() && migrationRun: // Remove the migration entry that creates unified alerting data. This is so when the feature @@ -213,11 +211,8 @@ type migration struct { sess *xorm.Session mg *migrator.Migrator - seenChannelUIDs map[string]struct{} - migratedChannelsPerOrg map[int64]map[*notificationChannel]struct{} - silences map[int64][]*pb.MeshSilence - portedChannelGroupsPerOrg map[int64]map[string]string // Org -> Channel group key -> receiver name. - lastReceiverID int // For the auto generated receivers. + seenChannelUIDs map[string]struct{} + silences map[int64][]*pb.MeshSilence } func (m *migration) SQL(dialect migrator.Dialect) string { @@ -247,21 +242,12 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { return err } - // allChannels: channelUID -> channelConfig - allChannelsPerOrg, defaultChannelsPerOrg, err := m.getNotificationChannelMap() - if err != nil { - return err - } - - amConfigPerOrg := make(amConfigsPerOrg, len(allChannelsPerOrg)) - err = m.addDefaultChannels(amConfigPerOrg, allChannelsPerOrg, defaultChannelsPerOrg) - if err != nil { - return err - } - // cache for folders created for dashboards that have custom permissions folderCache := make(map[string]*dashboard) + // Store of newly created rules to later create routes + rulesPerOrg := make(map[int64]map[string]dashAlert) + for _, da := range dashAlerts { newCond, err := transConditions(*da.ParsedSettings, da.OrgId, dsIDMap) if err != nil { @@ -358,11 +344,15 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { return err } - if _, ok := amConfigPerOrg[rule.OrgID]; !ok { - m.mg.Logger.Info("no configuration found", "org", rule.OrgID) + if _, ok := rulesPerOrg[rule.OrgID]; !ok { + rulesPerOrg[rule.OrgID] = make(map[string]dashAlert) + } + if _, ok := rulesPerOrg[rule.OrgID][rule.UID]; !ok { + rulesPerOrg[rule.OrgID][rule.UID] = da } else { - if err := m.updateReceiverAndRoute(allChannelsPerOrg, defaultChannelsPerOrg, da, rule, amConfigPerOrg[rule.OrgID]); err != nil { - return err + return MigrationError{ + Err: fmt.Errorf("duplicate generated rule UID"), + AlertId: da.Id, } } @@ -392,37 +382,20 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { } } - for orgID, amConfig := range amConfigPerOrg { - // Create a separate receiver for all the unmigrated channels. - err = m.addUnmigratedChannels(orgID, amConfig, allChannelsPerOrg[orgID], defaultChannelsPerOrg[orgID]) - if err != nil { - return err - } - - // 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 + for orgID := range rulesPerOrg { + if err := m.writeSilencesFile(orgID); err != nil { + m.mg.Logger.Error("alert migration error: failed to write silence file", "err", err) } + } + amConfigPerOrg, err := m.setupAlertmanagerConfigs(rulesPerOrg) + if err != nil { + return err + } + for orgID, amConfig := range amConfigPerOrg { if err := m.writeAlertmanagerConfig(orgID, amConfig); err != nil { return err } - - if err := m.writeSilencesFile(orgID); err != nil { - m.mg.Logger.Error("alert migration error: failed to write silence file", "err", err) - } } return nil diff --git a/pkg/services/sqlstore/migrations/ualert/ualert_test.go b/pkg/services/sqlstore/migrations/ualert/ualert_test.go index e5bdda6fc07..75abe7369fa 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert_test.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert_test.go @@ -1,11 +1,14 @@ package ualert import ( + "encoding/base64" "encoding/json" "fmt" "sort" "testing" + "github.com/prometheus/alertmanager/pkg/labels" + "github.com/stretchr/testify/require" "xorm.io/xorm" "github.com/grafana/grafana/pkg/components/simplejson" @@ -13,9 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/prometheus/alertmanager/pkg/labels" - - "github.com/stretchr/testify/require" ) var MigTitle = migTitle @@ -208,6 +208,18 @@ func configFromReceivers(t *testing.T, receivers []*PostableGrafanaReceiver) *Po } } +func (c *PostableUserConfig) EncryptSecureSettings() error { + for _, r := range c.AlertmanagerConfig.Receivers { + for _, gr := range r.GrafanaManagedReceivers { + encryptedData := GetEncryptedJsonData(gr.SecureSettings) + for k, v := range encryptedData { + gr.SecureSettings[k] = base64.StdEncoding.EncodeToString(v) + } + } + } + return nil +} + const invalidUri = "�6�M��)uk譹1(�h`$�o�N>mĕ����cS2�dh![ę� ���`csB�!��OSxP�{�" func Test_getAlertFolderNameFromDashboard(t *testing.T) {