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, - }) - - return nil - } - - // 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) + recv := &PostableApiReceiver{ + Name: c.Name, // Channel name is unique within an Org. + GrafanaManagedReceivers: []*PostableGrafanaReceiver{notifier}, } - } - // Add default channels that are not obsolete. - for _, c := range defaultChannels { - id := interface{}(c.Uid) - if c.Uid == "" { - id = c.ID + + receivers = append(receivers, recv) + + // 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 - } + return receiversMap, receivers, nil +} - chanKey, err := makeKeyForChannelGroup(filteredChannelUids) - if err != nil { - return nil, nil, err - } +// 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 - var receiverName string - - 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 + 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) + defaultRoute := &Route{ + Receiver: defaultReceiverName, + Routes: make([]*Route, 0), + } + + return defaultReceiver, defaultRoute, nil +} + +// 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 + } + + return nil, nil +} + +// 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, nil, err - } - route := &Route{ - Receiver: receiverName, - Matchers: Matchers{mat}, + return nil, err } - return receiver, route, 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) - } - } - - sort.Strings(uids) - return strings.Join(uids, "::sep::"), 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), - }, - }, + 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 + 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) } - 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 - } - - uid, ok := m.generateChannelUID() - if !ok { - return errors.New("failed to generate UID for notification channel") - } - - 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, - }) - } - receiver.GrafanaManagedReceivers = portedChannels - if len(portedChannels) > 0 { - amConfigs.AlertmanagerConfig.Receivers = append(amConfigs.AlertmanagerConfig.Receivers, receiver) } - return nil + 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 + } + + if len(filteredReceiverNames) == 0 || coveredByDefault(filteredReceiverNames) { + // Use the default route instead. + return nil + } + + // Add default receivers alongside rule-specific ones. + for n := range defaultReceivers { + filteredReceiverNames[n] = struct{}{} + } + + 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,34 @@ 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 - }, - alerts: []*models.Alert{ - createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier1"}), + createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false), }, + alerts: []*models.Alert{}, expected: map[int64]*ualert.PostableUserConfig{ int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ @@ -183,7 +191,49 @@ 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: "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{ + Route: &ualert.Route{ + 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"}}}, }, }, }, @@ -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,35 +320,9 @@ 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{}), - }, - expected: map[int64]*ualert.PostableUserConfig{ - int64(1): { - AlertmanagerConfig: ualert.PostableApiAlertingConfig{ - Route: &ualert.Route{ - 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: "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), }, alerts: []*models.Alert{}, expected: map[int64]*ualert.PostableUserConfig{ @@ -306,6 +332,29 @@ 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"}, + }, + }, + }, + }, + }, + { + name: "when unsupported channels, do not migrate them", + legacyChannels: []*models.AlertNotification{ + 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{ + 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"}, }, }, @@ -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,39 +382,22 @@ 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 - } - - if err := m.writeAlertmanagerConfig(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 + } + } + 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) {