forked from grafana.jool/grafana-jool
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
463 lines
14 KiB
463 lines
14 KiB
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"`
|
|
}
|
|
|