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"` }