mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Alerting: Validate contact point configuration during the migration This minimises the chances of generating broken configuration as part of the migration. Originally, we wanted to generate it and not produce a hard stop in Grafana but this strategy has the chance to avoid delivering notifications for our users. We now think it's better to hard stop the migration and let the user take care of resolving the configuration manually.
464 lines
14 KiB
Go
464 lines
14 KiB
Go
package ualert
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/prometheus/alertmanager/pkg/labels"
|
|
)
|
|
|
|
type notificationChannel struct {
|
|
ID int64 `xorm:"id"`
|
|
OrgID int64 `xorm:"org_id"`
|
|
Uid string `xorm:"uid"`
|
|
Name string `xorm:"name"`
|
|
Type string `xorm:"type"`
|
|
DisableResolveMessage bool `xorm:"disable_resolve_message"`
|
|
IsDefault bool `xorm:"is_default"`
|
|
Settings *simplejson.Json `xorm:"settings"`
|
|
SecureSettings SecureJsonData `xorm:"secure_settings"`
|
|
}
|
|
|
|
// channelsPerOrg maps notification channels per organisation
|
|
type channelsPerOrg map[int64]map[interface{}]*notificationChannel
|
|
|
|
// channelMap maps notification channels per organisation
|
|
type defaultChannelsPerOrg map[int64][]*notificationChannel
|
|
|
|
func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannelsPerOrg, error) {
|
|
q := `
|
|
SELECT id,
|
|
org_id,
|
|
uid,
|
|
name,
|
|
type,
|
|
disable_resolve_message,
|
|
is_default,
|
|
settings,
|
|
secure_settings
|
|
FROM
|
|
alert_notification
|
|
`
|
|
allChannels := []notificationChannel{}
|
|
err := m.sess.SQL(q).Find(&allChannels)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if len(allChannels) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
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.IsDefault {
|
|
defaultChannelsMap[c.OrgID] = append(defaultChannelsMap[c.OrgID], &allChannels[i])
|
|
}
|
|
}
|
|
|
|
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])
|
|
if err != nil {
|
|
return 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)
|
|
}
|
|
|
|
return 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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
portedChannels = append(portedChannels, &PostableGrafanaReceiver{
|
|
UID: uid,
|
|
Name: c.Name,
|
|
Type: c.Type,
|
|
DisableResolveMessage: c.DisableResolveMessage,
|
|
Settings: settings,
|
|
SecureSettings: decryptedSecureSettings,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove obsolete notification channels.
|
|
filteredChannelUids := make(map[interface{}]struct{})
|
|
for _, uid := range channelUids {
|
|
c, ok := allChannels[uid]
|
|
if ok {
|
|
// always store the channel UID to prevent duplicates
|
|
filteredChannelUids[c.Uid] = struct{}{}
|
|
} else {
|
|
m.mg.Logger.Warn("ignoring obsolete notification channel", "uid", uid)
|
|
}
|
|
}
|
|
// Add default channels that are not obsolete.
|
|
for _, c := range defaultChannels {
|
|
id := interface{}(c.Uid)
|
|
if c.Uid == "" {
|
|
id = c.ID
|
|
}
|
|
c, ok := allChannels[id]
|
|
if ok {
|
|
// always store the channel UID to prevent duplicates
|
|
filteredChannelUids[c.Uid] = struct{}{}
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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
|
|
}
|
|
} else {
|
|
for n := range filteredChannelUids {
|
|
if err := addChannel(allChannels[n]); 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,
|
|
}
|
|
}
|
|
|
|
n, v := getLabelForRouteMatching(ruleUid)
|
|
mat, err := labels.NewMatcher(labels.MatchEqual, n, v)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
route := &Route{
|
|
Receiver: receiverName,
|
|
Matchers: Matchers{mat},
|
|
}
|
|
|
|
return receiver, route, nil
|
|
}
|
|
|
|
// makeKeyForChannelGroup generates a unique for this group of channels UIDs.
|
|
func makeKeyForChannelGroup(channelUids map[interface{}]struct{}) (string, error) {
|
|
uids := make([]string, 0, len(channelUids))
|
|
for u := range channelUids {
|
|
switch uid := u.(type) {
|
|
case string:
|
|
uids = append(uids, uid)
|
|
case int, int32, int64:
|
|
uids = append(uids, fmt.Sprintf("%d", uid))
|
|
default:
|
|
// Should never happen.
|
|
return "", fmt.Errorf("unknown channel UID type: %T", u)
|
|
}
|
|
}
|
|
|
|
sort.Strings(uids)
|
|
return strings.Join(uids, "::sep::"), nil
|
|
}
|
|
|
|
// addDefaultChannels should be called before adding any other routes.
|
|
func (m *migration) addDefaultChannels(amConfigsPerOrg amConfigsPerOrg, allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg) error {
|
|
for orgID := range allChannels {
|
|
if _, ok := amConfigsPerOrg[orgID]; !ok {
|
|
amConfigsPerOrg[orgID] = &PostableUserConfig{
|
|
AlertmanagerConfig: PostableApiAlertingConfig{
|
|
Receivers: make([]*PostableApiReceiver, 0),
|
|
Route: &Route{
|
|
Routes: make([]*Route, 0),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
return 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",
|
|
}
|
|
for _, c := range allChannels {
|
|
if _, ok := m.migratedChannelsPerOrg[orgID]; !ok {
|
|
m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{})
|
|
}
|
|
_, ok := m.migratedChannelsPerOrg[orgID][c]
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (m *migration) generateChannelUID() (string, bool) {
|
|
for i := 0; i < 5; i++ {
|
|
gen := util.GenerateShortUID()
|
|
if _, ok := m.seenChannelUIDs[gen]; !ok {
|
|
m.seenChannelUIDs[gen] = struct{}{}
|
|
return gen, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// Some settings were migrated from settings to secure settings in between.
|
|
// See https://grafana.com/docs/grafana/latest/installation/upgrading/#ensure-encryption-of-existing-alert-notification-channel-secrets.
|
|
// migrateSettingsToSecureSettings takes care of that.
|
|
func migrateSettingsToSecureSettings(chanType string, settings *simplejson.Json, secureSettings SecureJsonData) (*simplejson.Json, map[string]string, error) {
|
|
keys := []string{}
|
|
switch chanType {
|
|
case "slack":
|
|
keys = []string{"url", "token"}
|
|
case "pagerduty":
|
|
keys = []string{"integrationKey"}
|
|
case "webhook":
|
|
keys = []string{"password"}
|
|
case "prometheus-alertmanager":
|
|
keys = []string{"basicAuthPassword"}
|
|
case "opsgenie":
|
|
keys = []string{"apiKey"}
|
|
case "telegram":
|
|
keys = []string{"bottoken"}
|
|
case "line":
|
|
keys = []string{"token"}
|
|
case "pushover":
|
|
keys = []string{"apiToken", "userKey"}
|
|
case "threema":
|
|
keys = []string{"api_secret"}
|
|
}
|
|
|
|
decryptedSecureSettings := secureSettings.Decrypt()
|
|
cloneSettings := simplejson.New()
|
|
settingsMap, err := settings.Map()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for k, v := range settingsMap {
|
|
cloneSettings.Set(k, v)
|
|
}
|
|
for _, k := range keys {
|
|
if v, ok := decryptedSecureSettings[k]; ok && v != "" {
|
|
continue
|
|
}
|
|
|
|
sv := cloneSettings.Get(k).MustString()
|
|
if sv != "" {
|
|
decryptedSecureSettings[k] = sv
|
|
cloneSettings.Del(k)
|
|
}
|
|
}
|
|
|
|
return cloneSettings, decryptedSecureSettings, nil
|
|
}
|
|
|
|
func getLabelForRouteMatching(ruleUID string) (string, string) {
|
|
return "rule_uid", ruleUID
|
|
}
|
|
|
|
func extractChannelIDs(d dashAlert) (channelUids []interface{}) {
|
|
// Extracting channel UID/ID.
|
|
for _, ui := range d.ParsedSettings.Notifications {
|
|
if ui.UID != "" {
|
|
channelUids = append(channelUids, ui.UID)
|
|
continue
|
|
}
|
|
// In certain circumstances, id is used instead of uid.
|
|
// We add this if there was no uid.
|
|
if ui.ID > 0 {
|
|
channelUids = append(channelUids, ui.ID)
|
|
}
|
|
}
|
|
|
|
return channelUids
|
|
}
|
|
|
|
// Below is a snapshot of all the config and supporting functions imported
|
|
// to avoid vendoring those packages.
|
|
|
|
type PostableUserConfig struct {
|
|
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
|
|
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
|
}
|
|
|
|
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"`
|
|
Receivers []*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
type Matchers labels.Matchers
|
|
|
|
func (m Matchers) MarshalJSON() ([]byte, error) {
|
|
if len(m) == 0 {
|
|
return nil, nil
|
|
}
|
|
result := make([]string, len(m))
|
|
for i, matcher := range m {
|
|
result[i] = matcher.String()
|
|
}
|
|
return json.Marshal(result)
|
|
}
|
|
|
|
type PostableApiReceiver struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
GrafanaManagedReceivers []*PostableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"`
|
|
}
|
|
|
|
type PostableGrafanaReceiver CreateAlertNotificationCommand
|
|
|
|
type CreateAlertNotificationCommand struct {
|
|
UID string `json:"uid"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
DisableResolveMessage bool `json:"disableResolveMessage"`
|
|
Settings *simplejson.Json `json:"settings"`
|
|
SecureSettings map[string]string `json:"secureSettings"`
|
|
}
|