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 <josue.abreu@gmail.com>
pull/48022/head^2
Matthew Jacobson 3 years ago committed by GitHub
parent 5f594addbf
commit 0301d956da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      pkg/services/ngalert/CHANGELOG.md
  2. 453
      pkg/services/sqlstore/migrations/ualert/channel.go
  3. 349
      pkg/services/sqlstore/migrations/ualert/channel_test.go
  4. 145
      pkg/services/sqlstore/migrations/ualert/migration_test.go
  5. 4
      pkg/services/sqlstore/migrations/ualert/testing.go
  6. 75
      pkg/services/sqlstore/migrations/ualert/ualert.go
  7. 18
      pkg/services/sqlstore/migrations/ualert/ualert_test.go

@ -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

@ -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

@ -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 &notificationChannel{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)
})
}
}

@ -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})
}

@ -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{}),
}
}

@ -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

@ -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 = "<EFBFBD>6<EFBFBD>M<EFBFBD><EFBFBD>)uk譹1(<EFBFBD>h`$<EFBFBD>o<EFBFBD>N>mĕ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>cS2<EFBFBD>dh![ę<EFBFBD> <EFBFBD><EFBFBD><EFBFBD>`csB<EFBFBD>!<EFBFBD><EFBFBD>OSxP<EFBFBD>{<EFBFBD>"
func Test_getAlertFolderNameFromDashboard(t *testing.T) {

Loading…
Cancel
Save