mirror of
https://github.com/grafana/grafana.git
synced 2025-09-22 23:24:55 +08:00
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>
This commit is contained in:
@ -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_last_consumed_tick_timestamp_seconds`
|
||||||
- `grafana_alerting_ticker_next_tick_timestamp_seconds`
|
- `grafana_alerting_ticker_next_tick_timestamp_seconds`
|
||||||
- `grafana_alerting_ticker_interval_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"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"github.com/prometheus/alertmanager/pkg/labels"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/prometheus/alertmanager/pkg/labels"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type notificationChannel struct {
|
type notificationChannel struct {
|
||||||
@ -26,11 +25,88 @@ type notificationChannel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// channelsPerOrg maps notification channels per organisation
|
// channelsPerOrg maps notification channels per organisation
|
||||||
type channelsPerOrg map[int64]map[interface{}]*notificationChannel
|
type channelsPerOrg map[int64][]*notificationChannel
|
||||||
|
|
||||||
// channelMap maps notification channels per organisation
|
// channelMap maps notification channels per organisation
|
||||||
type defaultChannelsPerOrg map[int64][]*notificationChannel
|
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) {
|
func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannelsPerOrg, error) {
|
||||||
q := `
|
q := `
|
||||||
SELECT id,
|
SELECT id,
|
||||||
@ -58,15 +134,13 @@ func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannels
|
|||||||
allChannelsMap := make(channelsPerOrg)
|
allChannelsMap := make(channelsPerOrg)
|
||||||
defaultChannelsMap := make(defaultChannelsPerOrg)
|
defaultChannelsMap := make(defaultChannelsPerOrg)
|
||||||
for i, c := range allChannels {
|
for i, c := range allChannels {
|
||||||
if _, ok := allChannelsMap[c.OrgID]; !ok { // new seen org
|
if c.Type == "hipchat" || c.Type == "sensu" {
|
||||||
allChannelsMap[c.OrgID] = make(map[interface{}]*notificationChannel)
|
m.mg.Logger.Error("alert migration error: discontinued notification channel found", "type", c.Type, "name", c.Name, "uid", c.Uid)
|
||||||
}
|
continue
|
||||||
if c.Uid != "" {
|
|
||||||
allChannelsMap[c.OrgID][c.Uid] = &allChannels[i]
|
|
||||||
}
|
|
||||||
if c.ID != 0 {
|
|
||||||
allChannelsMap[c.OrgID][c.ID] = &allChannels[i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allChannelsMap[c.OrgID] = append(allChannelsMap[c.OrgID], &allChannels[i])
|
||||||
|
|
||||||
if c.IsDefault {
|
if c.IsDefault {
|
||||||
defaultChannelsMap[c.OrgID] = append(defaultChannelsMap[c.OrgID], &allChannels[i])
|
defaultChannelsMap[c.OrgID] = append(defaultChannelsMap[c.OrgID], &allChannels[i])
|
||||||
}
|
}
|
||||||
@ -75,257 +149,200 @@ func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannels
|
|||||||
return allChannelsMap, defaultChannelsMap, nil
|
return allChannelsMap, defaultChannelsMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) updateReceiverAndRoute(allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg, da dashAlert, rule *alertRule, amConfig *PostableUserConfig) error {
|
// Create a notifier (PostableGrafanaReceiver) from a legacy notification channel
|
||||||
// Create receiver and route for this rule.
|
func (m *migration) createNotifier(c *notificationChannel) (*PostableGrafanaReceiver, error) {
|
||||||
if allChannels == nil {
|
uid, err := m.generateChannelUID()
|
||||||
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])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if recv != nil {
|
settings, secureSettings, err := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
|
||||||
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, recv)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
if route != nil {
|
|
||||||
amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, route)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
// Create one receiver for every unique notification channel.
|
||||||
portedChannels := []*PostableGrafanaReceiver{}
|
func (m *migration) createReceivers(allChannels []*notificationChannel) (map[uidOrID]*PostableApiReceiver, []*PostableApiReceiver, error) {
|
||||||
var receiver *PostableApiReceiver
|
var receivers []*PostableApiReceiver
|
||||||
|
receiversMap := make(map[uidOrID]*PostableApiReceiver)
|
||||||
addChannel := func(c *notificationChannel) error {
|
for _, c := range allChannels {
|
||||||
if c.Type == "hipchat" || c.Type == "sensu" {
|
notifier, err := m.createNotifier(c)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
portedChannels = append(portedChannels, &PostableGrafanaReceiver{
|
recv := &PostableApiReceiver{
|
||||||
UID: uid,
|
Name: c.Name, // Channel name is unique within an Org.
|
||||||
Name: c.Name,
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{notifier},
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Add default channels that are not obsolete.
|
receivers = append(receivers, recv)
|
||||||
for _, c := range defaultChannels {
|
|
||||||
id := interface{}(c.Uid)
|
// Store receivers for creating routes from alert rules later.
|
||||||
if c.Uid == "" {
|
if c.Uid != "" {
|
||||||
id = c.ID
|
receiversMap[c.Uid] = recv
|
||||||
}
|
}
|
||||||
c, ok := allChannels[id]
|
if c.ID != 0 {
|
||||||
if ok {
|
// In certain circumstances, the alert rule uses ID instead of uid. So, we add this to be able to lookup by ID in case.
|
||||||
// always store the channel UID to prevent duplicates
|
receiversMap[c.ID] = recv
|
||||||
filteredChannelUids[c.Uid] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filteredChannelUids) == 0 && ruleUid != "default_route" {
|
return receiversMap, receivers, nil
|
||||||
// We use the default route instead. No need to add additional route.
|
}
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
chanKey, err := makeKeyForChannelGroup(filteredChannelUids)
|
// 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.
|
||||||
if err != nil {
|
func (m *migration) createDefaultRouteAndReceiver(defaultChannels []*notificationChannel) (*PostableApiReceiver, *Route, error) {
|
||||||
return nil, nil, err
|
var defaultReceiver *PostableApiReceiver
|
||||||
}
|
|
||||||
|
|
||||||
var receiverName string
|
defaultReceiverName := "autogen-contact-point-default"
|
||||||
|
if len(defaultChannels) != 1 {
|
||||||
if _, ok := m.portedChannelGroupsPerOrg[orgID]; !ok {
|
// 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.
|
||||||
m.portedChannelGroupsPerOrg[orgID] = make(map[string]string)
|
// If we ever allow more than one receiver per route this won't be necessary.
|
||||||
}
|
defaultReceiver = &PostableApiReceiver{
|
||||||
if rn, ok := m.portedChannelGroupsPerOrg[orgID][chanKey]; ok {
|
Name: defaultReceiverName,
|
||||||
// We have ported these exact set of channels already. Re-use it.
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{},
|
||||||
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
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for n := range filteredChannelUids {
|
for _, c := range defaultChannels {
|
||||||
if err := addChannel(allChannels[n]); err != nil {
|
// Need to create a new notifier to prevent uid conflict.
|
||||||
|
defaultNotifier, err := m.createNotifier(c)
|
||||||
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ruleUid == "default_route" {
|
defaultReceiver.GrafanaManagedReceivers = append(defaultReceiver.GrafanaManagedReceivers, defaultNotifier)
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
} 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)
|
mat, err := labels.NewMatcher(labels.MatchEqual, n, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
|
||||||
route := &Route{
|
|
||||||
Receiver: receiverName,
|
|
||||||
Matchers: Matchers{mat},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return receiver, route, nil
|
var route *Route
|
||||||
}
|
if len(filteredReceiverNames) == 1 {
|
||||||
|
for name := range filteredReceiverNames {
|
||||||
// makeKeyForChannelGroup generates a unique for this group of channels UIDs.
|
route = &Route{
|
||||||
func makeKeyForChannelGroup(channelUids map[interface{}]struct{}) (string, error) {
|
Receiver: name,
|
||||||
uids := make([]string, 0, len(channelUids))
|
Matchers: Matchers{mat},
|
||||||
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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Default route and receiver.
|
} else {
|
||||||
recv, route, err := m.makeReceiverAndRoute("default_route", orgID, nil, defaultChannels[orgID], allChannels[orgID])
|
nestedRoutes := []*Route{}
|
||||||
if err != nil {
|
for name := range filteredReceiverNames {
|
||||||
// if one fails it will fail the migration
|
r := &Route{
|
||||||
return err
|
Receiver: name,
|
||||||
|
Matchers: Matchers{mat},
|
||||||
|
Continue: true,
|
||||||
|
}
|
||||||
|
nestedRoutes = append(nestedRoutes, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
if recv != nil {
|
route = &Route{
|
||||||
amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers = append(amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers, recv)
|
Matchers: Matchers{mat},
|
||||||
}
|
Routes: nestedRoutes,
|
||||||
if route != nil {
|
|
||||||
route.Matchers = nil // Don't need matchers for root route.
|
|
||||||
amConfigsPerOrg[orgID].AlertmanagerConfig.Route = route
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return route, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) addUnmigratedChannels(orgID int64, amConfigs *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error {
|
// Filter receivers to select those that were associated to the given rule as channels.
|
||||||
// Unmigrated channels.
|
func (m *migration) filterReceiversForAlert(da dashAlert, receivers map[uidOrID]*PostableApiReceiver, defaultReceivers map[string]struct{}) map[string]interface{} {
|
||||||
portedChannels := []*PostableGrafanaReceiver{}
|
channelIDs := extractChannelIDs(da)
|
||||||
receiver := &PostableApiReceiver{
|
if len(channelIDs) == 0 {
|
||||||
Name: "autogen-unlinked-channel-recv",
|
// If there are no channels associated, we use the default route.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
for _, c := range allChannels {
|
|
||||||
if _, ok := m.migratedChannelsPerOrg[orgID]; !ok {
|
// Filter receiver names.
|
||||||
m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{})
|
filteredReceiverNames := make(map[string]interface{})
|
||||||
}
|
for _, uidOrId := range channelIDs {
|
||||||
_, ok := m.migratedChannelsPerOrg[orgID][c]
|
recv, ok := receivers[uidOrId]
|
||||||
if ok {
|
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++ {
|
for i := 0; i < 5; i++ {
|
||||||
gen := util.GenerateShortUID()
|
gen := util.GenerateShortUID()
|
||||||
if _, ok := m.seenChannelUIDs[gen]; !ok {
|
if _, ok := m.seenChannelUIDs[gen]; !ok {
|
||||||
m.seenChannelUIDs[gen] = struct{}{}
|
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.
|
// 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"}
|
keys = []string{"api_secret"}
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptedSecureSettings := secureSettings.Decrypt()
|
newSecureSettings := secureSettings.Decrypt()
|
||||||
cloneSettings := simplejson.New()
|
cloneSettings := simplejson.New()
|
||||||
settingsMap, err := settings.Map()
|
settingsMap, err := settings.Map()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -364,25 +381,30 @@ func migrateSettingsToSecureSettings(chanType string, settings *simplejson.Json,
|
|||||||
cloneSettings.Set(k, v)
|
cloneSettings.Set(k, v)
|
||||||
}
|
}
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
if v, ok := decryptedSecureSettings[k]; ok && v != "" {
|
if v, ok := newSecureSettings[k]; ok && v != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sv := cloneSettings.Get(k).MustString()
|
sv := cloneSettings.Get(k).MustString()
|
||||||
if sv != "" {
|
if sv != "" {
|
||||||
decryptedSecureSettings[k] = sv
|
newSecureSettings[k] = sv
|
||||||
cloneSettings.Del(k)
|
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) {
|
func getLabelForRouteMatching(ruleUID string) (string, string) {
|
||||||
return "rule_uid", ruleUID
|
return "rule_uid", ruleUID
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractChannelIDs(d dashAlert) (channelUids []interface{}) {
|
func extractChannelIDs(d dashAlert) (channelUids []uidOrID) {
|
||||||
// Extracting channel UID/ID.
|
// Extracting channel UID/ID.
|
||||||
for _, ui := range d.ParsedSettings.Notifications {
|
for _, ui := range d.ParsedSettings.Notifications {
|
||||||
if ui.UID != "" {
|
if ui.UID != "" {
|
||||||
@ -409,18 +431,6 @@ type PostableUserConfig struct {
|
|||||||
|
|
||||||
type amConfigsPerOrg = map[int64]*PostableUserConfig
|
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 {
|
type PostableApiAlertingConfig struct {
|
||||||
Route *Route `yaml:"route,omitempty" json:"route,omitempty"`
|
Route *Route `yaml:"route,omitempty" json:"route,omitempty"`
|
||||||
Templates []string `yaml:"templates" json:"templates"`
|
Templates []string `yaml:"templates" json:"templates"`
|
||||||
@ -431,6 +441,7 @@ type Route struct {
|
|||||||
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"`
|
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"`
|
||||||
Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"`
|
Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"`
|
||||||
Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"`
|
Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"`
|
||||||
|
Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Matchers labels.Matchers
|
type Matchers labels.Matchers
|
||||||
|
349
pkg/services/sqlstore/migrations/ualert/channel_test.go
Normal file
349
pkg/services/sqlstore/migrations/ualert/channel_test.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -137,15 +137,18 @@ func TestDashAlertMigration(t *testing.T) {
|
|||||||
Route: &ualert.Route{
|
Route: &ualert.Route{
|
||||||
Receiver: "autogen-contact-point-default",
|
Receiver: "autogen-contact-point-default",
|
||||||
Routes: []*ualert.Route{
|
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: "notifier1", Matchers: createAlertNameMatchers("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")},
|
{Matchers: createAlertNameMatchers("alert2"), Routes: []*ualert.Route{
|
||||||
{Receiver: "autogen-contact-point-3", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert3")},
|
{Receiver: "notifier2", Matchers: createAlertNameMatchers("alert2"), Continue: true},
|
||||||
|
{Receiver: "notifier3", Matchers: createAlertNameMatchers("alert2"), Continue: true},
|
||||||
|
}},
|
||||||
|
{Receiver: "notifier3", Matchers: createAlertNameMatchers("alert3")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Receivers: []*ualert.PostableApiReceiver{
|
Receivers: []*ualert.PostableApiReceiver{
|
||||||
{Name: "autogen-contact-point-1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, // email
|
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
|
||||||
{Name: "autogen-contact-point-2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}, {Name: "notifier3", Type: "opsgenie"}}}, // slack+opsgenie
|
{Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}},
|
||||||
{Name: "autogen-contact-point-3", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}, // opsgenie
|
{Name: "notifier3", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}},
|
||||||
{Name: "autogen-contact-point-default"}, // empty default
|
{Name: "autogen-contact-point-default"}, // empty default
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -153,29 +156,34 @@ func TestDashAlertMigration(t *testing.T) {
|
|||||||
int64(2): {
|
int64(2): {
|
||||||
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
|
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
|
||||||
Route: &ualert.Route{
|
Route: &ualert.Route{
|
||||||
Receiver: "autogen-contact-point-default",
|
Receiver: "notifier6",
|
||||||
Routes: []*ualert.Route{
|
Routes: []*ualert.Route{
|
||||||
{Receiver: "autogen-contact-point-4", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert4")},
|
{Matchers: createAlertNameMatchers("alert4"), Routes: []*ualert.Route{
|
||||||
{Receiver: "autogen-contact-point-5", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert5")},
|
{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{
|
Receivers: []*ualert.PostableApiReceiver{
|
||||||
{Name: "autogen-contact-point-4", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier4", Type: "email"}, {Name: "notifier6", Type: "opsgenie"}}}, // email
|
{Name: "notifier4", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier4", Type: "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: "notifier5", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier5", Type: "slack"}}},
|
||||||
{Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}}, // empty default
|
{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{
|
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{
|
expected: map[int64]*ualert.PostableUserConfig{
|
||||||
int64(1): {
|
int64(1): {
|
||||||
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
|
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
|
||||||
@ -183,7 +191,49 @@ func TestDashAlertMigration(t *testing.T) {
|
|||||||
Receiver: "autogen-contact-point-default",
|
Receiver: "autogen-contact-point-default",
|
||||||
},
|
},
|
||||||
Receivers: []*ualert.PostableApiReceiver{
|
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), "notifier2", "slack", slackSettings, false),
|
||||||
createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, true), // default
|
createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, true), // default
|
||||||
},
|
},
|
||||||
alerts: []*models.Alert{
|
alerts: []*models.Alert{},
|
||||||
createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier2"}), // + notifier1, notifier3
|
|
||||||
},
|
|
||||||
expected: map[int64]*ualert.PostableUserConfig{
|
expected: map[int64]*ualert.PostableUserConfig{
|
||||||
int64(1): {
|
int64(1): {
|
||||||
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
|
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
|
||||||
Route: &ualert.Route{
|
Route: &ualert.Route{
|
||||||
Receiver: "autogen-contact-point-default",
|
Receiver: "autogen-contact-point-default",
|
||||||
Routes: []*ualert.Route{
|
|
||||||
{Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert1")},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Receivers: []*ualert.PostableApiReceiver{
|
Receivers: []*ualert.PostableApiReceiver{
|
||||||
{Name: "autogen-contact-point-1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}, {Name: "notifier3", Type: "opsgenie"}}},
|
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
|
||||||
{Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier3", Type: "opsgenie"}}},
|
{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",
|
Receiver: "autogen-contact-point-default",
|
||||||
},
|
},
|
||||||
Receivers: []*ualert.PostableApiReceiver{
|
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"}}},
|
{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{
|
legacyChannels: []*models.AlertNotification{
|
||||||
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
||||||
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
||||||
},
|
},
|
||||||
alerts: []*models.Alert{
|
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"}),
|
createAlert(t, int64(1), int64(1), int64(1), "alert2", []string{"notifier1", "notifier2"}),
|
||||||
},
|
},
|
||||||
expected: map[int64]*ualert.PostableUserConfig{
|
expected: map[int64]*ualert.PostableUserConfig{
|
||||||
@ -255,12 +303,16 @@ func TestDashAlertMigration(t *testing.T) {
|
|||||||
Route: &ualert.Route{
|
Route: &ualert.Route{
|
||||||
Receiver: "autogen-contact-point-default",
|
Receiver: "autogen-contact-point-default",
|
||||||
Routes: []*ualert.Route{
|
Routes: []*ualert.Route{
|
||||||
{Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert1")},
|
{Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")},
|
||||||
{Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert2")},
|
{Matchers: createAlertNameMatchers("alert2"), Routes: []*ualert.Route{
|
||||||
|
{Receiver: "notifier1", Matchers: createAlertNameMatchers("alert2"), Continue: true},
|
||||||
|
{Receiver: "notifier2", Matchers: createAlertNameMatchers("alert2"), Continue: true},
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Receivers: []*ualert.PostableApiReceiver{
|
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"},
|
{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{
|
legacyChannels: []*models.AlertNotification{
|
||||||
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true), // default
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
alerts: []*models.Alert{},
|
alerts: []*models.Alert{},
|
||||||
expected: map[int64]*ualert.PostableUserConfig{
|
expected: map[int64]*ualert.PostableUserConfig{
|
||||||
@ -306,6 +332,29 @@ func TestDashAlertMigration(t *testing.T) {
|
|||||||
Receiver: "autogen-contact-point-default",
|
Receiver: "autogen-contact-point-default",
|
||||||
},
|
},
|
||||||
Receivers: []*ualert.PostableApiReceiver{
|
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"},
|
{Name: "autogen-contact-point-default"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -327,11 +376,11 @@ func TestDashAlertMigration(t *testing.T) {
|
|||||||
Route: &ualert.Route{
|
Route: &ualert.Route{
|
||||||
Receiver: "autogen-contact-point-default",
|
Receiver: "autogen-contact-point-default",
|
||||||
Routes: []*ualert.Route{
|
Routes: []*ualert.Route{
|
||||||
{Receiver: "autogen-contact-point-1", Matchers: newMatchers(labels.MatchEqual, "alert_name", "alert1")},
|
{Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Receivers: []*ualert.PostableApiReceiver{
|
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"},
|
{Name: "autogen-contact-point-default"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -592,8 +641,8 @@ func boolPointer(b bool) *bool {
|
|||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMatchers creates a new ualert.Matchers given MatchType, name, and value.
|
// createAlertNameMatchers creates a temporary alert_name Matchers that will be replaced during runtime with the generated rule_uid.
|
||||||
func newMatchers(t labels.MatchType, n, v string) ualert.Matchers {
|
func createAlertNameMatchers(alertName string) ualert.Matchers {
|
||||||
matcher, _ := labels.NewMatcher(t, n, v)
|
matcher, _ := labels.NewMatcher(labels.MatchEqual, "alert_name", alertName)
|
||||||
return ualert.Matchers(labels.Matchers{matcher})
|
return ualert.Matchers(labels.Matchers{matcher})
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,6 @@ func newTestMigration(t *testing.T) *migration {
|
|||||||
|
|
||||||
Logger: log.New("test"),
|
Logger: log.New("test"),
|
||||||
},
|
},
|
||||||
migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
|
seenChannelUIDs: make(map[string]struct{}),
|
||||||
portedChannelGroupsPerOrg: make(map[int64]map[string]string),
|
|
||||||
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.Logger.Error("alert migration error: could not clear alert migration for removing data", "error", err)
|
||||||
}
|
}
|
||||||
mg.AddMigration(migTitle, &migration{
|
mg.AddMigration(migTitle, &migration{
|
||||||
seenChannelUIDs: make(map[string]struct{}),
|
seenChannelUIDs: make(map[string]struct{}),
|
||||||
migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
|
silences: make(map[int64][]*pb.MeshSilence),
|
||||||
portedChannelGroupsPerOrg: make(map[int64]map[string]string),
|
|
||||||
silences: make(map[int64][]*pb.MeshSilence),
|
|
||||||
})
|
})
|
||||||
case !mg.Cfg.UnifiedAlerting.IsEnabled() && migrationRun:
|
case !mg.Cfg.UnifiedAlerting.IsEnabled() && migrationRun:
|
||||||
// Remove the migration entry that creates unified alerting data. This is so when the feature
|
// 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
|
sess *xorm.Session
|
||||||
mg *migrator.Migrator
|
mg *migrator.Migrator
|
||||||
|
|
||||||
seenChannelUIDs map[string]struct{}
|
seenChannelUIDs map[string]struct{}
|
||||||
migratedChannelsPerOrg map[int64]map[*notificationChannel]struct{}
|
silences map[int64][]*pb.MeshSilence
|
||||||
silences map[int64][]*pb.MeshSilence
|
|
||||||
portedChannelGroupsPerOrg map[int64]map[string]string // Org -> Channel group key -> receiver name.
|
|
||||||
lastReceiverID int // For the auto generated receivers.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) SQL(dialect migrator.Dialect) string {
|
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
|
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
|
// cache for folders created for dashboards that have custom permissions
|
||||||
folderCache := make(map[string]*dashboard)
|
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 {
|
for _, da := range dashAlerts {
|
||||||
newCond, err := transConditions(*da.ParsedSettings, da.OrgId, dsIDMap)
|
newCond, err := transConditions(*da.ParsedSettings, da.OrgId, dsIDMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -358,11 +344,15 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := amConfigPerOrg[rule.OrgID]; !ok {
|
if _, ok := rulesPerOrg[rule.OrgID]; !ok {
|
||||||
m.mg.Logger.Info("no configuration found", "org", rule.OrgID)
|
rulesPerOrg[rule.OrgID] = make(map[string]dashAlert)
|
||||||
|
}
|
||||||
|
if _, ok := rulesPerOrg[rule.OrgID][rule.UID]; !ok {
|
||||||
|
rulesPerOrg[rule.OrgID][rule.UID] = da
|
||||||
} else {
|
} else {
|
||||||
if err := m.updateReceiverAndRoute(allChannelsPerOrg, defaultChannelsPerOrg, da, rule, amConfigPerOrg[rule.OrgID]); err != nil {
|
return MigrationError{
|
||||||
return err
|
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 {
|
for orgID := range rulesPerOrg {
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.writeSilencesFile(orgID); err != nil {
|
if err := m.writeSilencesFile(orgID); err != nil {
|
||||||
m.mg.Logger.Error("alert migration error: failed to write silence file", "err", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package ualert
|
package ualert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/alertmanager/pkg/labels"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"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/services/sqlstore/sqlutil"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/prometheus/alertmanager/pkg/labels"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var MigTitle = migTitle
|
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 = "<22>6<EFBFBD>M<EFBFBD><4D>)uk譹1(<28>h`$<24>o<EFBFBD>N>mĕ<6D><C495><EFBFBD><EFBFBD>cS2<53>dh![ę<> <09><><EFBFBD>`csB<73>!<21><>OSxP<78>{<7B>"
|
const invalidUri = "<22>6<EFBFBD>M<EFBFBD><4D>)uk譹1(<28>h`$<24>o<EFBFBD>N>mĕ<6D><C495><EFBFBD><EFBFBD>cS2<53>dh![ę<> <09><><EFBFBD>`csB<73>!<21><>OSxP<78>{<7B>"
|
||||||
|
|
||||||
func Test_getAlertFolderNameFromDashboard(t *testing.T) {
|
func Test_getAlertFolderNameFromDashboard(t *testing.T) {
|
||||||
|
Reference in New Issue
Block a user