mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: In migration, create one label per channel (#76527)
* In migration, create one label per channel This PR changes how routing is done by the legacy alerting migration. Previously, we created a single label on each alert rule that contained an array of contact point names. Ex: __contact__="slack legacy testing","slack legacy testing2" This label was then routed against a series of regex-matching policies with continue=true. Ex: __contacts__ =~ .*"slack legacy testing".* In the case of many contact points, this array could quickly become difficult to manage and difficult to grok at-a-glance. This PR replaces the single __contact__ label with multiple __legacy_c_{contactname}__ labels and simple equality-matching policies. These channel-specific policies are nested in a single route under the top-level route which matches against __legacy_use_channels__ = true for ease of organization. This should improve the experience for users wanting to keep the default migrated routing strategy but who also want to modify which contact points an alert sends to.
This commit is contained in:
parent
209619c8c1
commit
0424d44b39
@ -8,108 +8,117 @@ import (
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/tsdb/graphite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// ContactLabel is a private label created during migration and used in notification policies.
|
||||
// It stores a string array of all contact point names an alert rule should send to.
|
||||
// It was created as a means to simplify post-migration notification policies.
|
||||
ContactLabel = "__contacts__"
|
||||
)
|
||||
func addLabelsAndAnnotations(l log.Logger, alert *legacymodels.Alert, dashboardUID string, channels []*legacymodels.AlertNotification) (data.Labels, data.Labels) {
|
||||
tags := alert.GetTagsFromSettings()
|
||||
lbls := make(data.Labels, len(tags)+len(channels)+1)
|
||||
|
||||
func addMigrationInfo(da *migrationStore.DashAlert, dashboardUID string) (map[string]string, map[string]string) {
|
||||
tagsMap := simplejson.NewFromAny(da.ParsedSettings.AlertRuleTags).MustMap()
|
||||
lbls := make(map[string]string, len(tagsMap))
|
||||
|
||||
for k, v := range tagsMap {
|
||||
lbls[k] = simplejson.NewFromAny(v).MustString()
|
||||
for _, t := range tags {
|
||||
lbls[t.Key] = t.Value
|
||||
}
|
||||
|
||||
annotations := make(map[string]string, 3)
|
||||
// Add a label for routing
|
||||
lbls[ngmodels.MigratedUseLegacyChannelsLabel] = "true"
|
||||
for _, c := range channels {
|
||||
lbls[contactLabel(c.Name)] = "true"
|
||||
}
|
||||
|
||||
annotations := make(data.Labels, 4)
|
||||
annotations[ngmodels.DashboardUIDAnnotation] = dashboardUID
|
||||
annotations[ngmodels.PanelIDAnnotation] = fmt.Sprintf("%v", da.PanelID)
|
||||
annotations["__alertId__"] = fmt.Sprintf("%v", da.ID)
|
||||
annotations[ngmodels.PanelIDAnnotation] = fmt.Sprintf("%v", alert.PanelID)
|
||||
annotations[ngmodels.MigratedAlertIdAnnotation] = fmt.Sprintf("%v", alert.ID)
|
||||
|
||||
message := MigrateTmpl(l.New("field", "message"), alert.Message)
|
||||
annotations[ngmodels.MigratedMessageAnnotation] = message
|
||||
|
||||
return lbls, annotations
|
||||
}
|
||||
|
||||
// MigrateAlert migrates a single dashboard alert from legacy alerting to unified alerting.
|
||||
func (om *OrgMigration) migrateAlert(ctx context.Context, l log.Logger, da *migrationStore.DashAlert, info migmodels.DashboardUpgradeInfo) (*ngmodels.AlertRule, error) {
|
||||
// migrateAlert migrates a single dashboard alert from legacy alerting to unified alerting.
|
||||
func (om *OrgMigration) migrateAlert(ctx context.Context, l log.Logger, alert *legacymodels.Alert, info migmodels.DashboardUpgradeInfo) (*ngmodels.AlertRule, error) {
|
||||
l.Debug("Migrating alert rule to Unified Alerting")
|
||||
cond, err := transConditions(ctx, l, da, om.migrationStore)
|
||||
rawSettings, err := json.Marshal(alert.Settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get settings: %w", err)
|
||||
}
|
||||
var parsedSettings dashAlertSettings
|
||||
err = json.Unmarshal(rawSettings, &parsedSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse settings: %w", err)
|
||||
}
|
||||
cond, err := transConditions(ctx, l, parsedSettings, alert.OrgID, om.migrationStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transform conditions: %w", err)
|
||||
}
|
||||
|
||||
lbls, annotations := addMigrationInfo(da, info.DashboardUID)
|
||||
channels := om.extractChannels(l, parsedSettings)
|
||||
|
||||
message := MigrateTmpl(l.New("field", "message"), da.Message)
|
||||
annotations["message"] = message
|
||||
lbls, annotations := addLabelsAndAnnotations(l, alert, info.DashboardUID, channels)
|
||||
|
||||
data, err := migrateAlertRuleQueries(l, cond.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to migrate alert rule queries: %w", err)
|
||||
return nil, fmt.Errorf("queries: %w", err)
|
||||
}
|
||||
|
||||
isPaused := false
|
||||
if da.State == "paused" {
|
||||
if alert.State == "paused" {
|
||||
isPaused = true
|
||||
}
|
||||
|
||||
// Here we ensure that the alert rule title is unique within the folder.
|
||||
titleDeduplicator := om.titleDeduplicatorForFolder(info.NewFolderUID)
|
||||
name, err := titleDeduplicator.Deduplicate(da.Name)
|
||||
name, err := titleDeduplicator.Deduplicate(alert.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name != da.Name {
|
||||
l.Info(fmt.Sprintf("Alert rule title modified to be unique within the folder and fit within the maximum length of %d", store.AlertDefinitionMaxTitleLength), "old", da.Name, "new", name)
|
||||
if name != alert.Name {
|
||||
l.Info(fmt.Sprintf("Alert rule title modified to be unique within the folder and fit within the maximum length of %d", store.AlertDefinitionMaxTitleLength), "old", alert.Name, "new", name)
|
||||
}
|
||||
|
||||
dashUID := info.DashboardUID
|
||||
ar := &ngmodels.AlertRule{
|
||||
OrgID: da.OrgID,
|
||||
OrgID: alert.OrgID,
|
||||
Title: name,
|
||||
UID: util.GenerateShortUID(),
|
||||
Condition: cond.Condition,
|
||||
Data: data,
|
||||
IntervalSeconds: ruleAdjustInterval(da.Frequency),
|
||||
IntervalSeconds: ruleAdjustInterval(alert.Frequency),
|
||||
Version: 1,
|
||||
NamespaceUID: info.NewFolderUID,
|
||||
DashboardUID: &dashUID,
|
||||
PanelID: &da.PanelID,
|
||||
RuleGroup: groupName(ruleAdjustInterval(da.Frequency), info.DashboardName),
|
||||
For: da.For,
|
||||
PanelID: &alert.PanelID,
|
||||
RuleGroup: groupName(ruleAdjustInterval(alert.Frequency), info.DashboardName),
|
||||
For: alert.For,
|
||||
Updated: time.Now().UTC(),
|
||||
Annotations: annotations,
|
||||
Labels: lbls,
|
||||
RuleGroupIndex: 1, // Every rule is in its own group.
|
||||
IsPaused: isPaused,
|
||||
NoDataState: transNoData(l, da.ParsedSettings.NoDataState),
|
||||
ExecErrState: transExecErr(l, da.ParsedSettings.ExecutionErrorState),
|
||||
NoDataState: transNoData(l, parsedSettings.NoDataState),
|
||||
ExecErrState: transExecErr(l, parsedSettings.ExecutionErrorState),
|
||||
}
|
||||
|
||||
// Label for routing and silences.
|
||||
n, v := getLabelForSilenceMatching(ar.UID)
|
||||
ar.Labels[n] = v
|
||||
|
||||
if da.ParsedSettings.ExecutionErrorState == string(legacymodels.ExecutionErrorKeepState) {
|
||||
if parsedSettings.ExecutionErrorState == string(legacymodels.ExecutionErrorKeepState) {
|
||||
if err := om.addErrorSilence(ar); err != nil {
|
||||
om.log.Error("Alert migration error: failed to create silence for Error", "rule_name", ar.Title, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if da.ParsedSettings.NoDataState == string(legacymodels.NoDataKeepState) {
|
||||
if parsedSettings.NoDataState == string(legacymodels.NoDataKeepState) {
|
||||
if err := om.addNoDataSilence(ar); err != nil {
|
||||
om.log.Error("Alert migration error: failed to create silence for NoData", "rule_name", ar.Title, "err", err)
|
||||
}
|
||||
@ -220,7 +229,7 @@ func isPrometheusQuery(queryData map[string]json.RawMessage) (bool, error) {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.Unmarshal(ds, &datasource); err != nil {
|
||||
return false, fmt.Errorf("failed to parse datasource '%s': %w", string(ds), err)
|
||||
return false, fmt.Errorf("parse datasource '%s': %w", string(ds), err)
|
||||
}
|
||||
if datasource.Type == "" {
|
||||
return false, fmt.Errorf("missing type field '%s'", string(ds))
|
||||
@ -277,21 +286,29 @@ func truncate(daName string, length int) string {
|
||||
return daName
|
||||
}
|
||||
|
||||
func extractChannelIDs(d *migrationStore.DashAlert) (channelUids []migrationStore.UidOrID) {
|
||||
// Extracting channel UID/ID.
|
||||
for _, ui := range d.ParsedSettings.Notifications {
|
||||
if ui.UID != "" {
|
||||
channelUids = append(channelUids, ui.UID)
|
||||
continue
|
||||
// extractChannels extracts notification channels from the given legacy dashboard alert parsed settings.
|
||||
func (om *OrgMigration) extractChannels(l log.Logger, parsedSettings dashAlertSettings) []*legacymodels.AlertNotification {
|
||||
// Extracting channels.
|
||||
channels := make([]*legacymodels.AlertNotification, 0, len(parsedSettings.Notifications))
|
||||
for _, key := range parsedSettings.Notifications {
|
||||
// Either id or uid can be defined in the dashboard alert notification settings. See alerting.NewRuleFromDBAlert.
|
||||
if key.ID > 0 {
|
||||
if c, ok := om.channelCache.GetChannelByID(key.ID); ok {
|
||||
channels = append(channels, c)
|
||||
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
|
||||
if key.UID != "" {
|
||||
if c, ok := om.channelCache.GetChannelByUID(key.UID); ok {
|
||||
channels = append(channels, c)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
l.Warn("Failed to get alert notification, skipping", "notificationKey", key)
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
// groupName constructs a group name from the dashboard title and the interval. It truncates the dashboard title
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -14,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
)
|
||||
@ -97,30 +97,33 @@ func TestMigrateAlertRuleQueries(t *testing.T) {
|
||||
func TestAddMigrationInfo(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
alert *migrationStore.DashAlert
|
||||
alert *legacymodels.Alert
|
||||
dashboard string
|
||||
expectedLabels map[string]string
|
||||
expectedAnnotations map[string]string
|
||||
expectedLabels data.Labels
|
||||
expectedAnnotations data.Labels
|
||||
}{
|
||||
{
|
||||
name: "when alert rule tags are a JSON array, they're ignored.",
|
||||
alert: &migrationStore.DashAlert{Alert: &legacymodels.Alert{ID: 43, PanelID: 42}, ParsedSettings: &migrationStore.DashAlertSettings{AlertRuleTags: []string{"one", "two", "three", "four"}}},
|
||||
name: "when alert rule tags are a JSON array, they're ignored.",
|
||||
alert: &legacymodels.Alert{ID: 43, PanelID: 42, Message: "message", Settings: simplejson.NewFromAny(map[string]any{
|
||||
"alertRuleTags": []string{"one", "two", "three", "four"},
|
||||
})},
|
||||
dashboard: "dashboard",
|
||||
expectedLabels: map[string]string{},
|
||||
expectedAnnotations: map[string]string{"__alertId__": "43", "__dashboardUid__": "dashboard", "__panelId__": "42"},
|
||||
expectedLabels: data.Labels{models.MigratedUseLegacyChannelsLabel: "true"},
|
||||
expectedAnnotations: data.Labels{models.MigratedAlertIdAnnotation: "43", models.DashboardUIDAnnotation: "dashboard", models.PanelIDAnnotation: "42", "message": "message"},
|
||||
},
|
||||
{
|
||||
name: "when alert rule tags are a JSON object",
|
||||
alert: &migrationStore.DashAlert{Alert: &legacymodels.Alert{ID: 43, PanelID: 42}, ParsedSettings: &migrationStore.DashAlertSettings{AlertRuleTags: map[string]any{"key": "value", "key2": "value2"}}},
|
||||
dashboard: "dashboard",
|
||||
expectedLabels: map[string]string{"key": "value", "key2": "value2"},
|
||||
expectedAnnotations: map[string]string{"__alertId__": "43", "__dashboardUid__": "dashboard", "__panelId__": "42"},
|
||||
name: "when alert rule tags are a JSON object",
|
||||
alert: &legacymodels.Alert{ID: 43, PanelID: 42, Message: "message", Settings: simplejson.NewFromAny(map[string]any{
|
||||
"alertRuleTags": map[string]any{"key": "value", "key2": "value2"},
|
||||
})}, dashboard: "dashboard",
|
||||
expectedLabels: data.Labels{models.MigratedUseLegacyChannelsLabel: "true", "key": "value", "key2": "value2"},
|
||||
expectedAnnotations: data.Labels{models.MigratedAlertIdAnnotation: "43", models.DashboardUIDAnnotation: "dashboard", models.PanelIDAnnotation: "42", "message": "message"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
labels, annotations := addMigrationInfo(tc.alert, tc.dashboard)
|
||||
labels, annotations := addLabelsAndAnnotations(&logtest.Fake{}, tc.alert, tc.dashboard, nil)
|
||||
require.Equal(t, tc.expectedLabels, labels)
|
||||
require.Equal(t, tc.expectedAnnotations, annotations)
|
||||
})
|
||||
@ -132,7 +135,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
info := migmodels.DashboardUpgradeInfo{
|
||||
DashboardUID: "dashboarduid",
|
||||
DashboardName: "dashboardname",
|
||||
NewFolderUID: "ewfolderuid",
|
||||
NewFolderUID: "newfolderuid",
|
||||
NewFolderName: "newfoldername",
|
||||
}
|
||||
t.Run("when mapping rule names", func(t *testing.T) {
|
||||
@ -141,7 +144,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, da.Name, ar.Title)
|
||||
@ -153,7 +156,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
||||
@ -165,7 +168,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
||||
@ -173,7 +176,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da = createTestDashAlert()
|
||||
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
||||
|
||||
ar, err = m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err = m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
||||
@ -186,7 +189,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ar.IsPaused)
|
||||
})
|
||||
@ -197,7 +200,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.State = "paused"
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ar.IsPaused)
|
||||
})
|
||||
@ -206,9 +209,9 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
da.ParsedSettings.NoDataState = uuid.NewString()
|
||||
da.Settings.Set("noDataState", uuid.NewString())
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, models.NoData, ar.NoDataState)
|
||||
})
|
||||
@ -217,9 +220,9 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
da.ParsedSettings.ExecutionErrorState = uuid.NewString()
|
||||
da.Settings.Set("executionErrorState", uuid.NewString())
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, models.ErrorErrState, ar.ExecErrState)
|
||||
})
|
||||
@ -230,7 +233,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.Message = "Instance ${instance} is down"
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
require.Nil(t, err)
|
||||
expected :=
|
||||
"{{- $mergedLabels := mergeLabelValues $values -}}\n" +
|
||||
@ -279,7 +282,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("interval %ds should be %s", test.interval, test.expected), func(t *testing.T) {
|
||||
da.Frequency = test.interval
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("%s - %s", info.DashboardName, test.expected), ar.RuleGroup)
|
||||
@ -298,7 +301,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
NewFolderName: "newfoldername",
|
||||
}
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.RuleGroup, store.AlertRuleMaxRuleGroupNameLength)
|
||||
@ -307,12 +310,10 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func createTestDashAlert() migrationStore.DashAlert {
|
||||
return migrationStore.DashAlert{
|
||||
Alert: &legacymodels.Alert{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
},
|
||||
ParsedSettings: &migrationStore.DashAlertSettings{},
|
||||
func createTestDashAlert() *legacymodels.Alert {
|
||||
return &legacymodels.Alert{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
Settings: simplejson.New(),
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,9 @@ package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
@ -18,7 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
)
|
||||
@ -28,87 +25,31 @@ const (
|
||||
DisabledRepeatInterval = model.Duration(time.Duration(8736) * time.Hour) // 1y
|
||||
)
|
||||
|
||||
// channelReceiver is a convenience struct that contains a notificationChannel and its corresponding migrated PostableApiReceiver.
|
||||
type channelReceiver struct {
|
||||
channel *legacymodels.AlertNotification
|
||||
receiver *apimodels.PostableApiReceiver
|
||||
}
|
||||
|
||||
// setupAlertmanagerConfigs creates Alertmanager configs with migrated receivers and routes.
|
||||
func (om *OrgMigration) migrateChannels(allChannels []*legacymodels.AlertNotification, pairs []*AlertPair) (*apimodels.PostableUserConfig, error) {
|
||||
var defaultChannels []*legacymodels.AlertNotification
|
||||
var channels []*legacymodels.AlertNotification
|
||||
for _, c := range allChannels {
|
||||
if c.Type == "hipchat" || c.Type == "sensu" {
|
||||
om.log.Error("Alert migration error: discontinued notification channel found", "type", c.Type, "name", c.Name, "uid", c.UID)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.IsDefault {
|
||||
defaultChannels = append(defaultChannels, c)
|
||||
}
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
amConfig := &apimodels.PostableUserConfig{
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Receivers: make([]*apimodels.PostableApiReceiver, 0),
|
||||
},
|
||||
}
|
||||
|
||||
// migrateChannels creates Alertmanager configs with migrated receivers and routes.
|
||||
func (om *OrgMigration) migrateChannels(channels []*legacymodels.AlertNotification) (*migmodels.Alertmanager, error) {
|
||||
amConfig := migmodels.NewAlertmanager()
|
||||
empty := true
|
||||
// Create all newly migrated receivers from legacy notification channels.
|
||||
receiversMap, receivers, err := om.createReceivers(channels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create receiver: %w", err)
|
||||
}
|
||||
|
||||
// No need to create an Alertmanager configuration if there are no receivers left that aren't obsolete.
|
||||
if len(receivers) == 0 {
|
||||
om.log.Warn("No available receivers")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, cr := range receivers {
|
||||
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, cr.receiver)
|
||||
}
|
||||
|
||||
defaultReceivers := make(map[string]struct{})
|
||||
// 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 := om.createDefaultRouteAndReceiver(defaultChannels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create default route & receiver in orgId %d: %w", om.orgID, err)
|
||||
}
|
||||
amConfig.AlertmanagerConfig.Route = defaultRoute
|
||||
if defaultReceiver != nil {
|
||||
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, defaultReceiver)
|
||||
}
|
||||
|
||||
for _, cr := range receivers {
|
||||
route, err := createRoute(cr)
|
||||
for _, c := range channels {
|
||||
receiver, err := om.createReceiver(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create route for receiver %s in orgId %d: %w", cr.receiver.Name, om.orgID, err)
|
||||
if errors.Is(err, ErrDiscontinued) {
|
||||
om.log.Error("Alert migration error: discontinued notification channel found", "type", c.Type, "name", c.Name, "uid", c.UID)
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("channel '%s': %w", c.Name, err)
|
||||
}
|
||||
|
||||
amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, route)
|
||||
}
|
||||
|
||||
for _, pair := range pairs {
|
||||
channelUids := extractChannelIDs(pair.DashAlert)
|
||||
filteredReceiverNames := om.filterReceiversForAlert(pair.AlertRule.Title, channelUids, receiversMap, defaultReceivers)
|
||||
|
||||
if len(filteredReceiverNames) != 0 {
|
||||
// Only create a contact label if there are specific receivers, otherwise it defaults to the root-level route.
|
||||
pair.AlertRule.Labels[ContactLabel] = contactListToString(filteredReceiverNames)
|
||||
empty = false
|
||||
route, err := createRoute(c, receiver.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("channel '%s': %w", c.Name, err)
|
||||
}
|
||||
amConfig.AddRoute(route)
|
||||
amConfig.AddReceiver(receiver)
|
||||
}
|
||||
|
||||
// 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 := om.validateAlertmanagerConfig(amConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate AlertmanagerConfig in orgId %d: %w", om.orgID, err)
|
||||
if empty {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return amConfig, nil
|
||||
@ -145,23 +86,7 @@ func (om *OrgMigration) validateAlertmanagerConfig(config *apimodels.PostableUse
|
||||
return nil
|
||||
}
|
||||
|
||||
// contactListToString creates a sorted string representation of a given map (set) of receiver names. Each name will be comma-separated and double-quoted. Names should not contain double quotes.
|
||||
func contactListToString(m map[string]any) string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, quote(k))
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
return strings.Join(keys, ",")
|
||||
}
|
||||
|
||||
// quote will surround the given string in double quotes.
|
||||
func quote(s string) string {
|
||||
return `"` + s + `"`
|
||||
}
|
||||
|
||||
// Create a notifier (PostableGrafanaReceiver) from a legacy notification channel
|
||||
// createNotifier creates a PostableGrafanaReceiver from a legacy notification channel.
|
||||
func (om *OrgMigration) createNotifier(c *legacymodels.AlertNotification) (*apimodels.PostableGrafanaReceiver, error) {
|
||||
settings, secureSettings, err := om.migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
|
||||
if err != nil {
|
||||
@ -183,177 +108,74 @@ func (om *OrgMigration) createNotifier(c *legacymodels.AlertNotification) (*apim
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create one receiver for every unique notification channel.
|
||||
func (om *OrgMigration) createReceivers(allChannels []*legacymodels.AlertNotification) (map[migrationStore.UidOrID]*apimodels.PostableApiReceiver, []channelReceiver, error) {
|
||||
receivers := make([]channelReceiver, 0, len(allChannels))
|
||||
receiversMap := make(map[migrationStore.UidOrID]*apimodels.PostableApiReceiver)
|
||||
var ErrDiscontinued = errors.New("discontinued")
|
||||
|
||||
set := make(map[string]struct{}) // Used to deduplicate sanitized names.
|
||||
for _, c := range allChannels {
|
||||
notifier, err := om.createNotifier(c)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We remove double quotes because this character will be used as the separator in the ContactLabel. To prevent partial matches in the Route Matcher we choose to sanitize them early on instead of complicating the Matcher regex.
|
||||
sanitizedName := strings.ReplaceAll(c.Name, `"`, `_`)
|
||||
// There can be name collisions after we sanitize. We check for this and attempt to make the name unique again using a short hash of the original name.
|
||||
if _, ok := set[sanitizedName]; ok {
|
||||
sanitizedName = sanitizedName + fmt.Sprintf("_%.3x", md5.Sum([]byte(c.Name)))
|
||||
om.log.Warn("Alert contains duplicate contact name after sanitization, appending unique suffix", "type", c.Type, "name", c.Name, "new_name", sanitizedName, "uid", c.UID)
|
||||
}
|
||||
notifier.Name = sanitizedName
|
||||
|
||||
set[sanitizedName] = struct{}{}
|
||||
|
||||
cr := channelReceiver{
|
||||
channel: c,
|
||||
receiver: &apimodels.PostableApiReceiver{
|
||||
Receiver: config.Receiver{
|
||||
Name: sanitizedName, // Channel name is unique within an Org.
|
||||
},
|
||||
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{notifier},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
receivers = append(receivers, cr)
|
||||
|
||||
// Store receivers for creating routes from alert rules later.
|
||||
if c.UID != "" {
|
||||
receiversMap[c.UID] = cr.receiver
|
||||
}
|
||||
if c.ID != 0 {
|
||||
// In certain circumstances, the alert rule uses ID instead of uid. So, we add this to be able to lookup by ID in case.
|
||||
receiversMap[c.ID] = cr.receiver
|
||||
}
|
||||
// createReceiver creates a receiver from a legacy notification channel.
|
||||
func (om *OrgMigration) createReceiver(channel *legacymodels.AlertNotification) (*apimodels.PostableApiReceiver, error) {
|
||||
if channel.Type == "hipchat" || channel.Type == "sensu" {
|
||||
return nil, fmt.Errorf("'%s': %w", channel.Type, ErrDiscontinued)
|
||||
}
|
||||
|
||||
return receiversMap, receivers, nil
|
||||
}
|
||||
|
||||
// Create the root-level route with the default receiver. If no new receiver is created specifically for the root-level route, the returned receiver will be nil.
|
||||
func (om *OrgMigration) createDefaultRouteAndReceiver(defaultChannels []*legacymodels.AlertNotification) (*apimodels.PostableApiReceiver, *apimodels.Route, error) {
|
||||
defaultReceiverName := "autogen-contact-point-default"
|
||||
defaultRoute := &apimodels.Route{
|
||||
Receiver: defaultReceiverName,
|
||||
Routes: make([]*apimodels.Route, 0),
|
||||
GroupByStr: []string{ngmodels.FolderTitleLabel, model.AlertNameLabel}, // To keep parity with pre-migration notifications.
|
||||
RepeatInterval: nil,
|
||||
}
|
||||
newDefaultReceiver := &apimodels.PostableApiReceiver{
|
||||
Receiver: config.Receiver{
|
||||
Name: defaultReceiverName,
|
||||
},
|
||||
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{},
|
||||
},
|
||||
}
|
||||
|
||||
// Return early if there are no default channels
|
||||
if len(defaultChannels) == 0 {
|
||||
return newDefaultReceiver, defaultRoute, nil
|
||||
}
|
||||
|
||||
repeatInterval := DisabledRepeatInterval // If no channels have SendReminders enabled, we will use this large value as a pseudo-disable.
|
||||
if len(defaultChannels) > 1 {
|
||||
// If there are more than one default channels we create a separate contact group that is used only in the root policy. This is to simplify the migrated notification policy structure.
|
||||
// If we ever allow more than one receiver per route this won't be necessary.
|
||||
for _, c := range defaultChannels {
|
||||
// Need to create a new notifier to prevent uid conflict.
|
||||
defaultNotifier, err := om.createNotifier(c)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
newDefaultReceiver.GrafanaManagedReceivers = append(newDefaultReceiver.GrafanaManagedReceivers, defaultNotifier)
|
||||
|
||||
// Choose the lowest send reminder duration from all the notifiers to use for default route.
|
||||
if c.SendReminder && c.Frequency < time.Duration(repeatInterval) {
|
||||
repeatInterval = model.Duration(c.Frequency)
|
||||
}
|
||||
}
|
||||
} 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.
|
||||
defaultRoute.Receiver = defaultChannels[0].Name
|
||||
if defaultChannels[0].SendReminder {
|
||||
repeatInterval = model.Duration(defaultChannels[0].Frequency)
|
||||
}
|
||||
|
||||
// No need to create a new receiver.
|
||||
newDefaultReceiver = nil
|
||||
}
|
||||
defaultRoute.RepeatInterval = &repeatInterval
|
||||
|
||||
return newDefaultReceiver, defaultRoute, nil
|
||||
}
|
||||
|
||||
// Create one route per contact point, matching based on ContactLabel.
|
||||
func createRoute(cr channelReceiver) (*apimodels.Route, error) {
|
||||
// We create a regex matcher so that each alert rule need only have a single ContactLabel entry for all contact points it sends to.
|
||||
// For example, if an alert needs to send to contact1 and contact2 it will have ContactLabel=`"contact1","contact2"` and will match both routes looking
|
||||
// for `.*"contact1".*` and `.*"contact2".*`.
|
||||
|
||||
// We quote and escape here to ensure the regex will correctly match the ContactLabel on the alerts.
|
||||
name := fmt.Sprintf(`.*%s.*`, regexp.QuoteMeta(quote(cr.receiver.Name)))
|
||||
mat, err := labels.NewMatcher(labels.MatchRegexp, ContactLabel, name)
|
||||
notifier, err := om.createNotifier(channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apimodels.PostableApiReceiver{
|
||||
Receiver: config.Receiver{
|
||||
Name: channel.Name, // Channel name is unique within an Org.
|
||||
},
|
||||
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{notifier},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createRoute creates a route from a legacy notification channel, and matches using a label based on the channel UID.
|
||||
func createRoute(channel *legacymodels.AlertNotification, receiverName string) (*apimodels.Route, error) {
|
||||
// We create a matchers based on channel name so that we only need a single route per channel.
|
||||
// All channel routes are nested in a single route under the root. This is so we can keep the migrated channels separate
|
||||
// and organized.
|
||||
// Since default channels are attached to all alerts in legacy, we use a catch-all matcher after migration instead
|
||||
// of a specific label matcher.
|
||||
//
|
||||
// For example, if an alert needs to send to channel1 and channel2 it will have one label to route to the nested
|
||||
// policy and two channel-specific labels to route to the correct contact points:
|
||||
// - __legacy_use_channels__="true"
|
||||
// - __legacy_c_channel1__="true"
|
||||
// - __legacy_c_channel2__="true"
|
||||
//
|
||||
// If an alert needs to send to channel1 and the default channel, it will have one label to route to the nested
|
||||
// policy and one channel-specific label to route to channel1, and a catch-all policy will ensure it also routes to
|
||||
// the default channel.
|
||||
|
||||
label := contactLabel(channel.Name)
|
||||
mat, err := labels.NewMatcher(labels.MatchEqual, label, "true")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the channel is default, we create a catch-all matcher instead so this always matches.
|
||||
if channel.IsDefault {
|
||||
mat, _ = labels.NewMatcher(labels.MatchRegexp, model.AlertNameLabel, ".+")
|
||||
}
|
||||
|
||||
repeatInterval := DisabledRepeatInterval
|
||||
if cr.channel.SendReminder {
|
||||
repeatInterval = model.Duration(cr.channel.Frequency)
|
||||
if channel.SendReminder {
|
||||
repeatInterval = model.Duration(channel.Frequency)
|
||||
}
|
||||
|
||||
return &apimodels.Route{
|
||||
Receiver: cr.receiver.Name,
|
||||
Receiver: receiverName,
|
||||
ObjectMatchers: apimodels.ObjectMatchers{mat},
|
||||
Continue: true, // We continue so that each sibling contact point route can separately match.
|
||||
RepeatInterval: &repeatInterval,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Filter receivers to select those that were associated to the given rule as channels.
|
||||
func (om *OrgMigration) filterReceiversForAlert(name string, channelIDs []migrationStore.UidOrID, receivers map[migrationStore.UidOrID]*apimodels.PostableApiReceiver, defaultReceivers map[string]struct{}) map[string]any {
|
||||
if len(channelIDs) == 0 {
|
||||
// If there are no channels associated, we use the default route.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter receiver names.
|
||||
filteredReceiverNames := make(map[string]any)
|
||||
for _, uidOrId := range channelIDs {
|
||||
recv, ok := receivers[uidOrId]
|
||||
if ok {
|
||||
filteredReceiverNames[recv.Name] = struct{}{} // Deduplicate on contact point name.
|
||||
} else {
|
||||
om.log.Warn("Alert linked to obsolete notification channel, ignoring", "alert", name, "uid", uidOrId)
|
||||
}
|
||||
}
|
||||
|
||||
coveredByDefault := func(names map[string]any) 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
|
||||
// contactLabel creates a label matcher key used to route alerts to a contact point.
|
||||
func contactLabel(name string) string {
|
||||
return ngmodels.MigratedContactLabelPrefix + name + "__"
|
||||
}
|
||||
|
||||
var secureKeysToMigrate = map[string][]string{
|
||||
@ -406,7 +228,7 @@ func (om *OrgMigration) encryptSecureSettings(secureSettings map[string]string)
|
||||
for key, value := range secureSettings {
|
||||
encryptedData, err := om.encryptionService.Encrypt(context.Background(), []byte(value), secrets.WithoutScope())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt secure settings: %w", err)
|
||||
return fmt.Errorf("encrypt secure settings: %w", err)
|
||||
}
|
||||
secureSettings[key] = base64.StdEncoding.EncodeToString(encryptedData)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -18,90 +19,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func TestFilterReceiversForAlert(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
channelIds []migrationStore.UidOrID
|
||||
receivers map[migrationStore.UidOrID]*apimodels.PostableApiReceiver
|
||||
defaultReceivers map[string]struct{}
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "when an alert has multiple channels, each should filter for the correct receiver",
|
||||
channelIds: []migrationStore.UidOrID{"uid1", "uid2"},
|
||||
receivers: map[migrationStore.UidOrID]*apimodels.PostableApiReceiver{
|
||||
"uid1": createPostableApiReceiver("recv1", nil),
|
||||
"uid2": createPostableApiReceiver("recv2", nil),
|
||||
"uid3": createPostableApiReceiver("recv3", nil),
|
||||
},
|
||||
defaultReceivers: map[string]struct{}{},
|
||||
expected: map[string]any{
|
||||
"recv1": struct{}{},
|
||||
"recv2": struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when default receivers exist, they should be added to an alert's filtered receivers",
|
||||
channelIds: []migrationStore.UidOrID{"uid1"},
|
||||
receivers: map[migrationStore.UidOrID]*apimodels.PostableApiReceiver{
|
||||
"uid1": createPostableApiReceiver("recv1", nil),
|
||||
"uid2": createPostableApiReceiver("recv2", nil),
|
||||
"uid3": createPostableApiReceiver("recv3", nil),
|
||||
},
|
||||
defaultReceivers: map[string]struct{}{
|
||||
"recv2": {},
|
||||
},
|
||||
expected: map[string]any{
|
||||
"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",
|
||||
channelIds: []migrationStore.UidOrID{int64(42)},
|
||||
receivers: map[migrationStore.UidOrID]*apimodels.PostableApiReceiver{
|
||||
int64(42): createPostableApiReceiver("recv1", nil),
|
||||
},
|
||||
defaultReceivers: map[string]struct{}{},
|
||||
expected: map[string]any{
|
||||
"recv1": struct{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when an alert's receivers are covered by the defaults, return nil to use default receiver downstream",
|
||||
channelIds: []migrationStore.UidOrID{"uid1"},
|
||||
receivers: map[migrationStore.UidOrID]*apimodels.PostableApiReceiver{
|
||||
"uid1": createPostableApiReceiver("recv1", nil),
|
||||
"uid2": createPostableApiReceiver("recv2", nil),
|
||||
"uid3": createPostableApiReceiver("recv3", nil),
|
||||
},
|
||||
defaultReceivers: map[string]struct{}{
|
||||
"recv1": {},
|
||||
"recv2": {},
|
||||
},
|
||||
expected: nil, // recv1 is already a default
|
||||
},
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
res := m.filterReceiversForAlert("", tt.channelIds, tt.receivers, tt.defaultReceivers)
|
||||
|
||||
require.Equal(t, tt.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRoute(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
@ -110,12 +33,12 @@ func TestCreateRoute(t *testing.T) {
|
||||
expected *apimodels.Route
|
||||
}{
|
||||
{
|
||||
name: "when a receiver is passed in, the route should regex match based on quoted name with continue=true",
|
||||
channel: &legacymodels.AlertNotification{},
|
||||
recv: createPostableApiReceiver("recv1", nil),
|
||||
name: "when a receiver is passed in, the route should exact match based on channel uid with continue=true",
|
||||
channel: &legacymodels.AlertNotification{UID: "uid1", Name: "recv1"},
|
||||
recv: createPostableApiReceiver("uid1", "recv1"),
|
||||
expected: &apimodels.Route{
|
||||
Receiver: "recv1",
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"recv1".*`}},
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("recv1"), Value: "true"}},
|
||||
Routes: nil,
|
||||
Continue: true,
|
||||
GroupByStr: nil,
|
||||
@ -123,12 +46,12 @@ func TestCreateRoute(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "notification channel should be escaped for regex in the matcher",
|
||||
channel: &legacymodels.AlertNotification{},
|
||||
recv: createPostableApiReceiver(`. ^ $ * + - ? ( ) [ ] { } \ |`, nil),
|
||||
name: "notification channel labels matcher should work with special characters",
|
||||
channel: &legacymodels.AlertNotification{UID: "uid1", Name: `. ^ $ * + - ? ( ) [ ] { } \ |`},
|
||||
recv: createPostableApiReceiver("uid1", `. ^ $ * + - ? ( ) [ ] { } \ |`),
|
||||
expected: &apimodels.Route{
|
||||
Receiver: `. ^ $ * + - ? ( ) [ ] { } \ |`,
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"\. \^ \$ \* \+ - \? \( \) \[ \] \{ \} \\ \|".*`}},
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel(`. ^ $ * + - ? ( ) [ ] { } \ |`), Value: "true"}},
|
||||
Routes: nil,
|
||||
Continue: true,
|
||||
GroupByStr: nil,
|
||||
@ -137,11 +60,11 @@ func TestCreateRoute(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "when a channel has sendReminder=true, the route should use the frequency in repeat interval",
|
||||
channel: &legacymodels.AlertNotification{SendReminder: true, Frequency: time.Duration(42) * time.Hour},
|
||||
recv: createPostableApiReceiver("recv1", nil),
|
||||
channel: &legacymodels.AlertNotification{SendReminder: true, Frequency: time.Duration(42) * time.Hour, UID: "uid1", Name: "recv1"},
|
||||
recv: createPostableApiReceiver("uid1", "recv1"),
|
||||
expected: &apimodels.Route{
|
||||
Receiver: "recv1",
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"recv1".*`}},
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("recv1"), Value: "true"}},
|
||||
Routes: nil,
|
||||
Continue: true,
|
||||
GroupByStr: nil,
|
||||
@ -150,11 +73,11 @@ func TestCreateRoute(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "when a channel has sendReminder=false, the route should ignore the frequency in repeat interval and use DisabledRepeatInterval",
|
||||
channel: &legacymodels.AlertNotification{SendReminder: false, Frequency: time.Duration(42) * time.Hour},
|
||||
recv: createPostableApiReceiver("recv1", nil),
|
||||
channel: &legacymodels.AlertNotification{SendReminder: false, Frequency: time.Duration(42) * time.Hour, UID: "uid1", Name: "recv1"},
|
||||
recv: createPostableApiReceiver("uid1", "recv1"),
|
||||
expected: &apimodels.Route{
|
||||
Receiver: "recv1",
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"recv1".*`}},
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("recv1"), Value: "true"}},
|
||||
Routes: nil,
|
||||
Continue: true,
|
||||
GroupByStr: nil,
|
||||
@ -165,10 +88,7 @@ func TestCreateRoute(t *testing.T) {
|
||||
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res, err := createRoute(channelReceiver{
|
||||
channel: tt.channel,
|
||||
receiver: tt.recv,
|
||||
})
|
||||
res, err := createRoute(tt.channel, tt.recv.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Order of nested routes is not guaranteed.
|
||||
@ -189,78 +109,51 @@ func TestCreateRoute(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createNotChannel(t *testing.T, uid string, id int64, name string) *legacymodels.AlertNotification {
|
||||
func createNotChannel(t *testing.T, uid string, id int64, name string, isDefault bool, frequency time.Duration) *legacymodels.AlertNotification {
|
||||
t.Helper()
|
||||
return &legacymodels.AlertNotification{UID: uid, ID: id, Name: name, Settings: simplejson.New()}
|
||||
return &legacymodels.AlertNotification{
|
||||
OrgID: 1,
|
||||
UID: uid,
|
||||
ID: id,
|
||||
Name: name,
|
||||
Type: "email",
|
||||
SendReminder: frequency > 0,
|
||||
Frequency: frequency,
|
||||
Settings: simplejson.New(),
|
||||
IsDefault: isDefault,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
}
|
||||
|
||||
func createNotChannelWithReminder(t *testing.T, uid string, id int64, name string, frequency time.Duration) *legacymodels.AlertNotification {
|
||||
func createBasicNotChannel(t *testing.T, notType string) *legacymodels.AlertNotification {
|
||||
t.Helper()
|
||||
return &legacymodels.AlertNotification{UID: uid, ID: id, Name: name, SendReminder: true, Frequency: frequency, Settings: simplejson.New()}
|
||||
a := createNotChannel(t, "uid1", int64(1), "name1", false, 0)
|
||||
a.Type = notType
|
||||
return a
|
||||
}
|
||||
|
||||
func TestCreateReceivers(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
allChannels []*legacymodels.AlertNotification
|
||||
defaultChannels []*legacymodels.AlertNotification
|
||||
expRecvMap map[migrationStore.UidOrID]*apimodels.PostableApiReceiver
|
||||
expRecv []channelReceiver
|
||||
expErr error
|
||||
name string
|
||||
channel *legacymodels.AlertNotification
|
||||
expRecv *apimodels.PostableApiReceiver
|
||||
expErr error
|
||||
}{
|
||||
{
|
||||
name: "when given notification channels migrate them to receivers",
|
||||
allChannels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "name1"), createNotChannel(t, "uid2", int64(2), "name2")},
|
||||
expRecvMap: map[migrationStore.UidOrID]*apimodels.PostableApiReceiver{
|
||||
"uid1": createPostableApiReceiver("name1", []string{"name1"}),
|
||||
"uid2": createPostableApiReceiver("name2", []string{"name2"}),
|
||||
int64(1): createPostableApiReceiver("name1", []string{"name1"}),
|
||||
int64(2): createPostableApiReceiver("name2", []string{"name2"}),
|
||||
},
|
||||
expRecv: []channelReceiver{
|
||||
{
|
||||
channel: createNotChannel(t, "uid1", int64(1), "name1"),
|
||||
receiver: createPostableApiReceiver("name1", []string{"name1"}),
|
||||
},
|
||||
{
|
||||
channel: createNotChannel(t, "uid2", int64(2), "name2"),
|
||||
receiver: createPostableApiReceiver("name2", []string{"name2"}),
|
||||
},
|
||||
},
|
||||
name: "when given notification channels migrate them to receivers",
|
||||
channel: createNotChannel(t, "uid1", int64(1), "name1", false, 0),
|
||||
expRecv: createPostableApiReceiver("uid1", "name1"),
|
||||
},
|
||||
{
|
||||
name: "when given notification channel contains double quote sanitize with underscore",
|
||||
allChannels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "name\"1")},
|
||||
expRecvMap: map[migrationStore.UidOrID]*apimodels.PostableApiReceiver{
|
||||
"uid1": createPostableApiReceiver("name_1", []string{"name_1"}),
|
||||
int64(1): createPostableApiReceiver("name_1", []string{"name_1"}),
|
||||
},
|
||||
expRecv: []channelReceiver{
|
||||
{
|
||||
channel: createNotChannel(t, "uid1", int64(1), "name\"1"),
|
||||
receiver: createPostableApiReceiver("name_1", []string{"name_1"}),
|
||||
},
|
||||
},
|
||||
name: "when given hipchat return discontinued error",
|
||||
channel: createBasicNotChannel(t, "hipchat"),
|
||||
expErr: fmt.Errorf("'hipchat': %w", ErrDiscontinued),
|
||||
},
|
||||
{
|
||||
name: "when given notification channels collide after sanitization add short hash to end",
|
||||
allChannels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "name\"1"), createNotChannel(t, "uid2", int64(2), "name_1")},
|
||||
expRecvMap: map[migrationStore.UidOrID]*apimodels.PostableApiReceiver{
|
||||
"uid1": createPostableApiReceiver("name_1", []string{"name_1"}),
|
||||
"uid2": createPostableApiReceiver("name_1_dba13d", []string{"name_1_dba13d"}),
|
||||
int64(1): createPostableApiReceiver("name_1", []string{"name_1"}),
|
||||
int64(2): createPostableApiReceiver("name_1_dba13d", []string{"name_1_dba13d"}),
|
||||
},
|
||||
expRecv: []channelReceiver{
|
||||
{
|
||||
channel: createNotChannel(t, "uid1", int64(1), "name\"1"),
|
||||
receiver: createPostableApiReceiver("name_1", []string{"name_1"}),
|
||||
},
|
||||
{
|
||||
channel: createNotChannel(t, "uid2", int64(2), "name_1"),
|
||||
receiver: createPostableApiReceiver("name_1_dba13d", []string{"name_1_dba13d"}),
|
||||
},
|
||||
},
|
||||
name: "when given sensu return discontinued error",
|
||||
channel: createBasicNotChannel(t, "sensu"),
|
||||
expErr: fmt.Errorf("'sensu': %w", ErrDiscontinued),
|
||||
},
|
||||
}
|
||||
|
||||
@ -269,26 +162,14 @@ func TestCreateReceivers(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
recvMap, recvs, err := m.createReceivers(tt.allChannels)
|
||||
recv, err := m.createReceiver(tt.channel)
|
||||
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.receiver.GrafanaManagedReceivers {
|
||||
not.UID = ""
|
||||
not.Settings = nil
|
||||
not.SecureSettings = nil
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expRecvMap, recvMap)
|
||||
require.ElementsMatch(t, tt.expRecv, recvs)
|
||||
require.Equal(t, tt.expRecv, recv)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -473,115 +354,136 @@ func TestMigrateNotificationChannelSecureSettings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDefaultRouteAndReceiver(t *testing.T) {
|
||||
func TestSetupAlertmanagerConfig(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
amConfig *apimodels.PostableUserConfig
|
||||
defaultChannels []*legacymodels.AlertNotification
|
||||
expRecv *apimodels.PostableApiReceiver
|
||||
expRoute *apimodels.Route
|
||||
expErr error
|
||||
name string
|
||||
channels []*legacymodels.AlertNotification
|
||||
amConfig *apimodels.PostableUserConfig
|
||||
expErr error
|
||||
}{
|
||||
{
|
||||
name: "when given multiple default notification channels migrate them to a single receiver",
|
||||
defaultChannels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "name1"), createNotChannel(t, "uid2", int64(2), "name2")},
|
||||
expRecv: createPostableApiReceiver("autogen-contact-point-default", []string{"name1", "name2"}),
|
||||
expRoute: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
Routes: make([]*apimodels.Route, 0),
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
RepeatInterval: durationPointer(DisabledRepeatInterval),
|
||||
name: "when given multiple notification channels migrate them to receivers",
|
||||
channels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "notifier1", false, 0), createNotChannel(t, "uid2", int64(2), "notifier2", false, 0)},
|
||||
amConfig: &apimodels.PostableUserConfig{
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{}}},
|
||||
createPostableApiReceiver("uid1", "notifier1"),
|
||||
createPostableApiReceiver("uid2", "notifier2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when given multiple default notification channels migrate them to a single receiver with RepeatInterval set to be the minimum of all channel frequencies",
|
||||
defaultChannels: []*legacymodels.AlertNotification{
|
||||
createNotChannelWithReminder(t, "uid1", int64(1), "name1", time.Duration(42)),
|
||||
createNotChannelWithReminder(t, "uid2", int64(2), "name2", time.Duration(100000)),
|
||||
},
|
||||
expRecv: createPostableApiReceiver("autogen-contact-point-default", []string{"name1", "name2"}),
|
||||
expRoute: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
Routes: make([]*apimodels.Route, 0),
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
RepeatInterval: durationPointer(model.Duration(42)),
|
||||
name: "when given default notification channels migrate them to a routes with catchall matcher",
|
||||
channels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "notifier1", false, 0), createNotChannel(t, "uid2", int64(2), "notifier2", true, 0)},
|
||||
amConfig: &apimodels.PostableUserConfig{
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{}}},
|
||||
createPostableApiReceiver("uid1", "notifier1"),
|
||||
createPostableApiReceiver("uid2", "notifier2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when given no default notification channels create a single empty receiver for default",
|
||||
defaultChannels: []*legacymodels.AlertNotification{},
|
||||
expRecv: createPostableApiReceiver("autogen-contact-point-default", nil),
|
||||
expRoute: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
Routes: make([]*apimodels.Route, 0),
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
RepeatInterval: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when given a single default notification channels don't create a new default receiver",
|
||||
defaultChannels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "name1")},
|
||||
expRecv: nil,
|
||||
expRoute: &apimodels.Route{
|
||||
Receiver: "name1",
|
||||
Routes: make([]*apimodels.Route, 0),
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
RepeatInterval: durationPointer(DisabledRepeatInterval),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when given a single default notification channel with SendReminder=true, use the channels Frequency as the RepeatInterval",
|
||||
defaultChannels: []*legacymodels.AlertNotification{createNotChannelWithReminder(t, "uid1", int64(1), "name1", time.Duration(42))},
|
||||
expRecv: nil,
|
||||
expRoute: &apimodels.Route{
|
||||
Receiver: "name1",
|
||||
Routes: make([]*apimodels.Route, 0),
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
RepeatInterval: durationPointer(model.Duration(42)),
|
||||
name: "when given notification channels with SendReminder true migrate them to a route with frequency set",
|
||||
channels: []*legacymodels.AlertNotification{createNotChannel(t, "uid1", int64(1), "notifier1", false, time.Duration(42)), createNotChannel(t, "uid2", int64(2), "notifier2", false, time.Duration(43))},
|
||||
amConfig: &apimodels.PostableUserConfig{
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(42)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(43)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{}}},
|
||||
createPostableApiReceiver("uid1", "notifier1"),
|
||||
createPostableApiReceiver("uid2", "notifier2")},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
recv, route, err := m.createDefaultRouteAndReceiver(tt.defaultChannels)
|
||||
am, err := m.migrateChannels(tt.channels)
|
||||
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
|
||||
}
|
||||
amConfig := am.Config
|
||||
opts := []cmp.Option{
|
||||
cmpopts.IgnoreUnexported(apimodels.PostableUserConfig{}, labels.Matcher{}),
|
||||
cmpopts.SortSlices(func(a, b *apimodels.Route) bool { return a.Receiver < b.Receiver }),
|
||||
}
|
||||
if !cmp.Equal(tt.amConfig, amConfig, opts...) {
|
||||
t.Errorf("Unexpected Config: %v", cmp.Diff(tt.amConfig, amConfig, opts...))
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expRecv, recv)
|
||||
require.Equal(t, tt.expRoute, route)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createPostableApiReceiver(name string, integrationNames []string) *apimodels.PostableApiReceiver {
|
||||
integrations := make([]*apimodels.PostableGrafanaReceiver, 0, len(integrationNames))
|
||||
for _, integrationName := range integrationNames {
|
||||
integrations = append(integrations, &apimodels.PostableGrafanaReceiver{Name: integrationName})
|
||||
}
|
||||
func createPostableApiReceiver(uid string, name string) *apimodels.PostableApiReceiver {
|
||||
return &apimodels.PostableApiReceiver{
|
||||
Receiver: config.Receiver{
|
||||
Name: name,
|
||||
},
|
||||
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: integrations,
|
||||
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{
|
||||
{
|
||||
UID: uid,
|
||||
Type: "email",
|
||||
Name: name,
|
||||
Settings: apimodels.RawMessage("{}"),
|
||||
SecureSettings: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -22,11 +22,53 @@ import (
|
||||
// It is defined in pkg/expr/service.go as "DatasourceType"
|
||||
const expressionDatasourceUID = "__expr__"
|
||||
|
||||
// dashAlertSettings is a type for the JSON that is in the settings field of
|
||||
// the alert table.
|
||||
type dashAlertSettings struct {
|
||||
NoDataState string `json:"noDataState"`
|
||||
ExecutionErrorState string `json:"executionErrorState"`
|
||||
Conditions []dashAlertCondition `json:"conditions"`
|
||||
AlertRuleTags any `json:"alertRuleTags"`
|
||||
Notifications []notificationKey `json:"notifications"`
|
||||
}
|
||||
|
||||
// notificationKey is the object that represents the Notifications array in legacymodels.Alert.Settings.
|
||||
// At least one of ID or UID should always be present, otherwise the legacy channel was invalid.
|
||||
type notificationKey struct {
|
||||
UID string `json:"uid,omitempty"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// dashAlertingConditionJSON is like classic.ClassicConditionJSON except that it
|
||||
// includes the model property with the query.
|
||||
type dashAlertCondition struct {
|
||||
Evaluator evaluator `json:"evaluator"`
|
||||
|
||||
Operator struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"operator"`
|
||||
|
||||
Query struct {
|
||||
Params []string `json:"params"`
|
||||
DatasourceID int64 `json:"datasourceId"`
|
||||
Model json.RawMessage
|
||||
} `json:"query"`
|
||||
|
||||
Reducer struct {
|
||||
// Params []any `json:"params"` (Unused)
|
||||
Type string `json:"type"`
|
||||
}
|
||||
}
|
||||
|
||||
type evaluator struct {
|
||||
Params []float64 `json:"params"`
|
||||
Type string `json:"type"` // e.g. "gt"
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func transConditions(ctx context.Context, l log.Logger, alert *migrationStore.DashAlert, store migrationStore.Store) (*condition, error) {
|
||||
func transConditions(ctx context.Context, l log.Logger, set dashAlertSettings, orgID int64, store migrationStore.Store) (*condition, error) {
|
||||
// TODO: needs a significant refactor to reduce complexity.
|
||||
usr := getMigrationUser(alert.OrgID)
|
||||
set := alert.ParsedSettings
|
||||
usr := getMigrationUser(orgID)
|
||||
|
||||
refIDtoCondIdx := make(map[string][]int) // a map of original refIds to their corresponding condition index
|
||||
for i, cond := range set.Conditions {
|
||||
@ -202,10 +244,10 @@ func transConditions(ctx context.Context, l log.Logger, alert *migrationStore.Da
|
||||
}
|
||||
|
||||
// build the new classic condition pointing our new equivalent queries
|
||||
conditions := make([]classicConditionJSON, len(set.Conditions))
|
||||
conditions := make([]classicCondition, len(set.Conditions))
|
||||
for i, cond := range set.Conditions {
|
||||
newCond := classicConditionJSON{}
|
||||
newCond.Evaluator = migrationStore.ConditionEvalJSON{
|
||||
newCond := classicCondition{}
|
||||
newCond.Evaluator = evaluator{
|
||||
Type: cond.Evaluator.Type,
|
||||
Params: cond.Evaluator.Params,
|
||||
}
|
||||
@ -221,12 +263,12 @@ func transConditions(ctx context.Context, l log.Logger, alert *migrationStore.Da
|
||||
return nil, err
|
||||
}
|
||||
newCond.Condition = ccRefID // set the alert condition to point to the classic condition
|
||||
newCond.OrgID = alert.OrgID
|
||||
newCond.OrgID = orgID
|
||||
|
||||
exprModel := struct {
|
||||
Type string `json:"type"`
|
||||
RefID string `json:"refId"`
|
||||
Conditions []classicConditionJSON `json:"conditions"`
|
||||
Type string `json:"type"`
|
||||
RefID string `json:"refId"`
|
||||
Conditions []classicCondition `json:"conditions"`
|
||||
}{
|
||||
"classic_conditions",
|
||||
ccRefID,
|
||||
@ -282,7 +324,7 @@ func getNewRefID(refIDs map[string][]int) (string, error) {
|
||||
}
|
||||
return sR, nil
|
||||
}
|
||||
return "", fmt.Errorf("failed to generate unique RefID")
|
||||
return "", errors.New("failed to generate unique RefID")
|
||||
}
|
||||
|
||||
// getRelativeDuration turns the alerting durations for dashboard conditions
|
||||
@ -333,8 +375,8 @@ func getTo(to string) (time.Duration, error) {
|
||||
return -d, nil
|
||||
}
|
||||
|
||||
type classicConditionJSON struct {
|
||||
Evaluator migrationStore.ConditionEvalJSON `json:"evaluator"`
|
||||
type classicCondition struct {
|
||||
Evaluator evaluator `json:"evaluator"`
|
||||
|
||||
Operator struct {
|
||||
Type string `json:"type"`
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -20,9 +19,9 @@ func TestCondTransMultiCondOnSingleQuery(t *testing.T) {
|
||||
// Here we are testing that we got a query that is referenced by multiple conditions, all conditions get set correctly.
|
||||
ordID := int64(1)
|
||||
|
||||
settings := store.DashAlertSettings{}
|
||||
settings := dashAlertSettings{}
|
||||
|
||||
cond1 := store.DashAlertCondition{}
|
||||
cond1 := dashAlertCondition{}
|
||||
cond1.Evaluator.Params = []float64{20}
|
||||
cond1.Evaluator.Type = "lt"
|
||||
cond1.Operator.Type = "and"
|
||||
@ -35,7 +34,7 @@ func TestCondTransMultiCondOnSingleQuery(t *testing.T) {
|
||||
}
|
||||
cond1.Reducer.Type = "avg"
|
||||
|
||||
cond2 := store.DashAlertCondition{}
|
||||
cond2 := dashAlertCondition{}
|
||||
cond2.Evaluator.Params = []float64{500}
|
||||
cond2.Evaluator.Type = "gt"
|
||||
cond2.Operator.Type = "or"
|
||||
@ -48,7 +47,7 @@ func TestCondTransMultiCondOnSingleQuery(t *testing.T) {
|
||||
}
|
||||
cond2.Reducer.Type = "avg"
|
||||
|
||||
settings.Conditions = []store.DashAlertCondition{cond1, cond2}
|
||||
settings.Conditions = []dashAlertCondition{cond1, cond2}
|
||||
|
||||
alertQuery1 := models.AlertQuery{
|
||||
RefID: "A",
|
||||
@ -69,13 +68,8 @@ func TestCondTransMultiCondOnSingleQuery(t *testing.T) {
|
||||
Data: []models.AlertQuery{alertQuery1, alertQuery2},
|
||||
}
|
||||
|
||||
dashAlert := store.DashAlert{ParsedSettings: &settings}
|
||||
dashAlert.Alert = &legacymodels.Alert{
|
||||
OrgID: ordID,
|
||||
}
|
||||
|
||||
migrationStore := store.NewTestMigrationStore(t, db.InitTestDB(t), &setting.Cfg{})
|
||||
c, err := transConditions(context.Background(), &logtest.Fake{}, &dashAlert, migrationStore)
|
||||
c, err := transConditions(context.Background(), &logtest.Fake{}, settings, ordID, migrationStore)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, c)
|
||||
@ -86,9 +80,9 @@ func TestCondTransExtended(t *testing.T) {
|
||||
// generated correctly all subqueries for each offset. RefID A exists twice with a different offset (cond1, cond4).
|
||||
ordID := int64(1)
|
||||
|
||||
settings := store.DashAlertSettings{}
|
||||
settings := dashAlertSettings{}
|
||||
|
||||
cond1 := store.DashAlertCondition{}
|
||||
cond1 := dashAlertCondition{}
|
||||
cond1.Evaluator.Params = []float64{-500000}
|
||||
cond1.Evaluator.Type = "lt"
|
||||
cond1.Operator.Type = "and"
|
||||
@ -101,7 +95,7 @@ func TestCondTransExtended(t *testing.T) {
|
||||
}
|
||||
cond1.Reducer.Type = "diff"
|
||||
|
||||
cond2 := store.DashAlertCondition{}
|
||||
cond2 := dashAlertCondition{}
|
||||
cond2.Evaluator.Params = []float64{
|
||||
-0.01,
|
||||
0.01,
|
||||
@ -117,7 +111,7 @@ func TestCondTransExtended(t *testing.T) {
|
||||
}
|
||||
cond2.Reducer.Type = "diff"
|
||||
|
||||
cond3 := store.DashAlertCondition{}
|
||||
cond3 := dashAlertCondition{}
|
||||
cond3.Evaluator.Params = []float64{
|
||||
-500000,
|
||||
}
|
||||
@ -132,7 +126,7 @@ func TestCondTransExtended(t *testing.T) {
|
||||
}
|
||||
cond3.Reducer.Type = "diff"
|
||||
|
||||
cond4 := store.DashAlertCondition{}
|
||||
cond4 := dashAlertCondition{}
|
||||
cond4.Evaluator.Params = []float64{
|
||||
1000000,
|
||||
}
|
||||
@ -147,7 +141,7 @@ func TestCondTransExtended(t *testing.T) {
|
||||
}
|
||||
cond4.Reducer.Type = "last"
|
||||
|
||||
settings.Conditions = []store.DashAlertCondition{cond1, cond2, cond3, cond4}
|
||||
settings.Conditions = []dashAlertCondition{cond1, cond2, cond3, cond4}
|
||||
|
||||
alertQuery1 := models.AlertQuery{
|
||||
RefID: "A",
|
||||
@ -189,13 +183,8 @@ func TestCondTransExtended(t *testing.T) {
|
||||
Data: []models.AlertQuery{alertQuery1, alertQuery2, alertQuery3, alertQuery4, alertQuery5},
|
||||
}
|
||||
|
||||
dashAlert := store.DashAlert{ParsedSettings: &settings}
|
||||
dashAlert.Alert = &legacymodels.Alert{
|
||||
OrgID: ordID,
|
||||
}
|
||||
|
||||
migrationStore := store.NewTestMigrationStore(t, db.InitTestDB(t), &setting.Cfg{})
|
||||
c, err := transConditions(context.Background(), &logtest.Fake{}, &dashAlert, migrationStore)
|
||||
c, err := transConditions(context.Background(), &logtest.Fake{}, settings, ordID, migrationStore)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, c)
|
||||
|
@ -151,8 +151,8 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
legacyChannels []*models.AlertNotification
|
||||
alerts []*models.Alert
|
||||
|
||||
expected map[int64]*apimodels.PostableUserConfig
|
||||
expErr error
|
||||
expected map[int64]*apimodels.PostableUserConfig
|
||||
expErrors []string
|
||||
}{
|
||||
{
|
||||
name: "general multi-org, multi-alert, multi-channel migration",
|
||||
@ -169,7 +169,7 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
createAlert(t, 1, 1, 2, "alert2", []string{"notifier2", "notifier3"}),
|
||||
createAlert(t, 1, 2, 3, "alert3", []string{"notifier3"}),
|
||||
createAlert(t, 2, 3, 1, "alert4", []string{"notifier4"}),
|
||||
createAlert(t, 2, 3, 2, "alert5", []string{"notifier4", "notifier5", "notifier6"}),
|
||||
createAlert(t, 2, 3, 2, "alert5", []string{"notifier4", "notifier5"}),
|
||||
createAlert(t, 2, 4, 3, "alert6", []string{}),
|
||||
},
|
||||
expected: map[int64]*apimodels.PostableUserConfig{
|
||||
@ -179,37 +179,47 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier3", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier3".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier3", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier3"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
RepeatInterval: nil,
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier3"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}}, // empty default
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
int64(2): {
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "notifier6",
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier4", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier4".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier5", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier5".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier6", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier6".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier6", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier4", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier4"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier5", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier5"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
RepeatInterval: durationPointer(DisabledRepeatInterval),
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier6"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier4"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier4", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier5"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier5", Type: "slack"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier6"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -228,36 +238,17 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
RepeatInterval: nil,
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{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]*apimodels.PostableUserConfig{
|
||||
int64(1): {
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "notifier1",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
RepeatInterval: durationPointer(DisabledRepeatInterval),
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
},
|
||||
},
|
||||
@ -265,31 +256,7 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when single default channel with SendReminder, use channel Frequency as RepeatInterval",
|
||||
legacyChannels: []*models.AlertNotification{
|
||||
createAlertNotificationWithReminder(t, int64(1), "notifier1", "email", emailSettings, true, true, time.Duration(1)*time.Hour),
|
||||
},
|
||||
alerts: []*models.Alert{},
|
||||
expected: map[int64]*apimodels.PostableUserConfig{
|
||||
int64(1): {
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "notifier1",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(model.Duration(time.Duration(1) * time.Hour))},
|
||||
},
|
||||
RepeatInterval: durationPointer(model.Duration(time.Duration(1) * time.Hour)),
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when multiple default channels, add them to autogen-contact-point-default as well",
|
||||
name: "when multiple default channels, they all have catch-all matchers",
|
||||
legacyChannels: []*models.AlertNotification{
|
||||
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true),
|
||||
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, true),
|
||||
@ -302,78 +269,25 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
RepeatInterval: durationPointer(DisabledRepeatInterval),
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when multiple default channels with SendReminder, use minimum channel frequency as RepeatInterval",
|
||||
legacyChannels: []*models.AlertNotification{
|
||||
createAlertNotificationWithReminder(t, int64(1), "notifier1", "email", emailSettings, true, true, time.Duration(1)*time.Hour),
|
||||
createAlertNotificationWithReminder(t, int64(1), "notifier2", "slack", slackSettings, true, true, time.Duration(30)*time.Minute),
|
||||
},
|
||||
alerts: []*models.Alert{},
|
||||
expected: map[int64]*apimodels.PostableUserConfig{
|
||||
int64(1): {
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(model.Duration(time.Duration(1) * time.Hour))},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(model.Duration(time.Duration(30) * time.Minute))},
|
||||
},
|
||||
RepeatInterval: durationPointer(model.Duration(time.Duration(30) * time.Minute)),
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when default channels exist alongside non-default, add only defaults to autogen-contact-point-default",
|
||||
legacyChannels: []*models.AlertNotification{
|
||||
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true), // default
|
||||
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
||||
createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, true), // default
|
||||
},
|
||||
alerts: []*models.Alert{},
|
||||
expected: map[int64]*apimodels.PostableUserConfig{
|
||||
int64(1): {
|
||||
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
||||
Config: apimodels.Config{Route: &apimodels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier3", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier3".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
RepeatInterval: durationPointer(DisabledRepeatInterval),
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier3"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier3", Type: "opsgenie"}}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when alerts share channels, only create one receiver per legacy channel",
|
||||
legacyChannels: []*models.AlertNotification{
|
||||
@ -391,14 +305,20 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -417,12 +337,18 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -443,12 +369,18 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -470,25 +402,58 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
Receiver: "autogen-contact-point-default",
|
||||
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
{
|
||||
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
||||
Continue: true,
|
||||
Routes: []*apimodels.Route{
|
||||
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Receivers: []*apimodels.PostableApiReceiver{
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
||||
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
||||
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed channel migration fails upgrade",
|
||||
legacyChannels: []*models.AlertNotification{
|
||||
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
||||
createAlertNotification(t, int64(1), "notifier2", "slack", brokenSettings, false),
|
||||
},
|
||||
alerts: []*models.Alert{
|
||||
createAlert(t, 1, 1, 1, "alert1", []string{"notifier1"}),
|
||||
},
|
||||
expErrors: []string{"channel 'notifier2'"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer teardown(t, x, service)
|
||||
setupLegacyAlertsTables(t, x, tt.legacyChannels, tt.alerts, nil, nil)
|
||||
dashes := []*dashboards.Dashboard{
|
||||
createDashboard(t, 1, 1, "dash1-1", 5, nil),
|
||||
createDashboard(t, 2, 1, "dash2-1", 5, nil),
|
||||
createDashboard(t, 3, 2, "dash3-2", 6, nil),
|
||||
createDashboard(t, 4, 2, "dash4-2", 6, nil),
|
||||
}
|
||||
folders := []*dashboards.Dashboard{
|
||||
createFolder(t, 5, 1, "folder5-1"),
|
||||
createFolder(t, 6, 2, "folder6-2"),
|
||||
}
|
||||
setupLegacyAlertsTables(t, x, tt.legacyChannels, tt.alerts, folders, dashes)
|
||||
|
||||
err := service.Run(context.Background())
|
||||
if len(tt.expErrors) > 0 {
|
||||
for _, expErr := range tt.expErrors {
|
||||
require.ErrorContains(t, err, expErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for orgId := range tt.expected {
|
||||
@ -526,12 +491,15 @@ func TestAMConfigMigration(t *testing.T) {
|
||||
|
||||
// TestDashAlertMigration tests the execution of the migration specifically for alert rules.
|
||||
func TestDashAlertMigration(t *testing.T) {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
x := sqlStore.GetEngine()
|
||||
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
||||
withDefaults := func(lbls map[string]string) map[string]string {
|
||||
lbls[ngModels.MigratedUseLegacyChannelsLabel] = "true"
|
||||
return lbls
|
||||
}
|
||||
|
||||
t.Run("when DashAlertMigration create ContactLabel on migrated AlertRules", func(t *testing.T) {
|
||||
defer teardown(t, x, service)
|
||||
sqlStore := db.InitTestDB(t)
|
||||
x := sqlStore.GetEngine()
|
||||
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
||||
legacyChannels := []*models.AlertNotification{
|
||||
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
||||
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
||||
@ -545,19 +513,20 @@ func TestDashAlertMigration(t *testing.T) {
|
||||
createAlert(t, 1, 1, 2, "alert2", []string{"notifier2", "notifier3"}),
|
||||
createAlert(t, 1, 2, 3, "alert3", []string{"notifier3"}),
|
||||
createAlert(t, 2, 3, 1, "alert4", []string{"notifier4"}),
|
||||
createAlert(t, 2, 3, 2, "alert5", []string{"notifier4", "notifier5", "notifier6"}),
|
||||
createAlert(t, 2, 3, 2, "alert5", []string{"notifier4", "notifier5"}),
|
||||
createAlert(t, 2, 4, 3, "alert6", []string{}),
|
||||
}
|
||||
expected := map[int64]map[string]*ngModels.AlertRule{
|
||||
int64(1): {
|
||||
"alert1": {Labels: map[string]string{ContactLabel: `"notifier1"`}},
|
||||
"alert2": {Labels: map[string]string{ContactLabel: `"notifier2","notifier3"`}},
|
||||
"alert3": {Labels: map[string]string{ContactLabel: `"notifier3"`}},
|
||||
"alert1": {Labels: withDefaults(map[string]string{contactLabel("notifier1"): "true"})},
|
||||
"alert2": {Labels: withDefaults(map[string]string{contactLabel("notifier2"): "true", contactLabel("notifier3"): "true"})},
|
||||
"alert3": {Labels: withDefaults(map[string]string{contactLabel("notifier3"): "true"})},
|
||||
},
|
||||
int64(2): {
|
||||
"alert4": {Labels: map[string]string{ContactLabel: `"notifier4","notifier6"`}},
|
||||
"alert5": {Labels: map[string]string{ContactLabel: `"notifier4","notifier5","notifier6"`}},
|
||||
"alert6": {Labels: map[string]string{}},
|
||||
// Don't include default channels.
|
||||
"alert4": {Labels: withDefaults(map[string]string{contactLabel("notifier4"): "true"})},
|
||||
"alert5": {Labels: withDefaults(map[string]string{contactLabel("notifier4"): "true", contactLabel("notifier5"): "true"})},
|
||||
"alert6": {Labels: withDefaults(map[string]string{})},
|
||||
},
|
||||
}
|
||||
dashes := []*dashboards.Dashboard{
|
||||
@ -579,46 +548,20 @@ func TestDashAlertMigration(t *testing.T) {
|
||||
expectedRulesMap := expected[orgId]
|
||||
require.Len(t, rules, len(expectedRulesMap))
|
||||
for _, r := range rules {
|
||||
require.Equal(t, expectedRulesMap[r.Title].Labels[ContactLabel], r.Labels[ContactLabel])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("when DashAlertMigration create ContactLabel with sanitized name if name contains double quote", func(t *testing.T) {
|
||||
defer teardown(t, x, service)
|
||||
legacyChannels := []*models.AlertNotification{
|
||||
createAlertNotification(t, int64(1), "notif\"ier1", "email", emailSettings, false),
|
||||
}
|
||||
alerts := []*models.Alert{
|
||||
createAlert(t, 1, 1, 1, "alert1", []string{"notif\"ier1"}),
|
||||
}
|
||||
expected := map[int64]map[string]*ngModels.AlertRule{
|
||||
int64(1): {
|
||||
"alert1": {Labels: map[string]string{ContactLabel: `"notif_ier1"`}},
|
||||
},
|
||||
}
|
||||
dashes := []*dashboards.Dashboard{
|
||||
createDashboard(t, 1, 1, "dash1-1", 5, nil),
|
||||
}
|
||||
folders := []*dashboards.Dashboard{
|
||||
createFolder(t, 5, 1, "folder5-1"),
|
||||
}
|
||||
setupLegacyAlertsTables(t, x, legacyChannels, alerts, folders, dashes)
|
||||
err := service.Run(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
for orgId := range expected {
|
||||
rules := getAlertRules(t, x, orgId)
|
||||
expectedRulesMap := expected[orgId]
|
||||
require.Len(t, rules, len(expectedRulesMap))
|
||||
for _, r := range rules {
|
||||
require.Equal(t, expectedRulesMap[r.Title].Labels[ContactLabel], r.Labels[ContactLabel])
|
||||
delete(r.Labels, "rule_uid") // Not checking this here.
|
||||
exp := expectedRulesMap[r.Title].Labels
|
||||
require.Lenf(t, r.Labels, len(exp), "rule doesn't have correct number of labels: %s", r.Title)
|
||||
for l := range r.Labels {
|
||||
require.Equal(t, exp[l], r.Labels[l])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("when folder is missing put alert in General folder", func(t *testing.T) {
|
||||
defer teardown(t, x, service)
|
||||
sqlStore := db.InitTestDB(t)
|
||||
x := sqlStore.GetEngine()
|
||||
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
||||
o := createOrg(t, 1)
|
||||
folder1 := createFolder(t, 1, o.ID, "folder-1")
|
||||
dash1 := createDashboard(t, 3, o.ID, "dash1", folder1.ID, nil)
|
||||
@ -652,6 +595,75 @@ func TestDashAlertMigration(t *testing.T) {
|
||||
require.Equal(t, expectedFolder.UID, rule.NamespaceUID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("when alert notification settings contain different combinations of id and uid", func(t *testing.T) {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
x := sqlStore.GetEngine()
|
||||
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
||||
legacyChannels := []*models.AlertNotification{
|
||||
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
||||
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
||||
createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, false),
|
||||
createAlertNotification(t, int64(2), "notifier4", "email", emailSettings, false),
|
||||
createAlertNotification(t, int64(2), "notifier5", "slack", slackSettings, false),
|
||||
createAlertNotification(t, int64(2), "notifier6", "opsgenie", opsgenieSettings, true), // default
|
||||
}
|
||||
alerts := []*models.Alert{
|
||||
createAlert(t, 1, 1, 1, "alert1", nil),
|
||||
createAlert(t, 1, 1, 2, "alert2", nil),
|
||||
createAlert(t, 1, 2, 3, "alert3", nil),
|
||||
createAlert(t, 2, 3, 1, "alert4", nil),
|
||||
createAlert(t, 2, 3, 2, "alert5", nil),
|
||||
createAlert(t, 2, 4, 3, "alert6", nil),
|
||||
}
|
||||
alerts[0].Settings.Set("notifications", []notificationKey{{UID: "notifier1"}})
|
||||
alerts[1].Settings.Set("notifications", []notificationKey{{ID: 2}, {UID: "notifier3"}})
|
||||
alerts[2].Settings.Set("notifications", []notificationKey{{ID: 3, UID: "notifier4"}}) // This shouldn't happen, but if it does we choose the ID.
|
||||
alerts[3].Settings.Set("notifications", []notificationKey{{ID: -99}}) // Unknown ID
|
||||
alerts[4].Settings.Set("notifications", []notificationKey{{UID: "unknown"}}) // Unknown UID
|
||||
alerts[5].Settings.Set("notifications", []notificationKey{{ID: -99}, {UID: "unknown"}, {UID: "notifier4"}, {ID: 5}}) // Mixed unknown and known.
|
||||
|
||||
expected := map[int64]map[string]*ngModels.AlertRule{
|
||||
int64(1): {
|
||||
"alert1": {Labels: withDefaults(map[string]string{contactLabel("notifier1"): "true"})},
|
||||
"alert2": {Labels: withDefaults(map[string]string{contactLabel("notifier2"): "true", contactLabel("notifier3"): "true"})},
|
||||
"alert3": {Labels: withDefaults(map[string]string{contactLabel("notifier3"): "true"})},
|
||||
},
|
||||
int64(2): {
|
||||
// Don't include default channels.
|
||||
"alert4": {Labels: withDefaults(map[string]string{})},
|
||||
"alert5": {Labels: withDefaults(map[string]string{})},
|
||||
"alert6": {Labels: withDefaults(map[string]string{contactLabel("notifier4"): "true", contactLabel("notifier5"): "true"})},
|
||||
},
|
||||
}
|
||||
dashes := []*dashboards.Dashboard{
|
||||
createDashboard(t, 1, 1, "dash1-1", 5, nil),
|
||||
createDashboard(t, 2, 1, "dash2-1", 5, nil),
|
||||
createDashboard(t, 3, 2, "dash3-2", 6, nil),
|
||||
createDashboard(t, 4, 2, "dash4-2", 6, nil),
|
||||
}
|
||||
folders := []*dashboards.Dashboard{
|
||||
createFolder(t, 5, 1, "folder5-1"),
|
||||
createFolder(t, 6, 2, "folder6-2"),
|
||||
}
|
||||
setupLegacyAlertsTables(t, x, legacyChannels, alerts, folders, dashes)
|
||||
err := service.Run(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
for orgId := range expected {
|
||||
rules := getAlertRules(t, x, orgId)
|
||||
expectedRulesMap := expected[orgId]
|
||||
require.Len(t, rules, len(expectedRulesMap))
|
||||
for _, r := range rules {
|
||||
delete(r.Labels, "rule_uid") // Not checking this here.
|
||||
exp := expectedRulesMap[r.Title].Labels
|
||||
require.Lenf(t, r.Labels, len(exp), "rule doesn't have correct number of labels: %s", r.Title)
|
||||
for l := range r.Labels {
|
||||
require.Equal(t, exp[l], r.Labels[l])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDashAlertQueryMigration tests the execution of the migration specifically for alert rule queries.
|
||||
@ -676,11 +688,11 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
return createAlertQueryWithModel(refId, ds, from, to, fmt.Sprintf(newQueryModel, "", dur.Milliseconds(), refId))
|
||||
}
|
||||
|
||||
createClassicConditionQuery := func(refId string, conditions []classicConditionJSON) ngModels.AlertQuery {
|
||||
createClassicConditionQuery := func(refId string, conditions []classicCondition) ngModels.AlertQuery {
|
||||
exprModel := struct {
|
||||
Type string `json:"type"`
|
||||
RefID string `json:"refId"`
|
||||
Conditions []classicConditionJSON `json:"conditions"`
|
||||
Type string `json:"type"`
|
||||
RefID string `json:"refId"`
|
||||
Conditions []classicCondition `json:"conditions"`
|
||||
}{
|
||||
"classic_conditions",
|
||||
refId,
|
||||
@ -699,9 +711,9 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
return q
|
||||
}
|
||||
|
||||
cond := func(refId string, reducer string, evalType string, thresh float64) classicConditionJSON {
|
||||
return classicConditionJSON{
|
||||
Evaluator: migrationStore.ConditionEvalJSON{Params: []float64{thresh}, Type: evalType},
|
||||
cond := func(refId string, reducer string, evalType string, thresh float64) classicCondition {
|
||||
return classicCondition{
|
||||
Evaluator: evaluator{Params: []float64{thresh}, Type: evalType},
|
||||
Operator: struct {
|
||||
Type string `json:"type"`
|
||||
}{Type: "and"},
|
||||
@ -734,7 +746,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
Annotations: map[string]string{
|
||||
"message": "message",
|
||||
},
|
||||
Labels: map[string]string{},
|
||||
Labels: map[string]string{ngModels.MigratedUseLegacyChannelsLabel: "true"},
|
||||
IsPaused: false,
|
||||
}
|
||||
|
||||
@ -755,6 +767,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
|
||||
expectedFolder *dashboards.Dashboard
|
||||
expected map[int64][]*ngModels.AlertRule
|
||||
expErrors []string
|
||||
}
|
||||
|
||||
tc := []testcase{
|
||||
@ -762,15 +775,15 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "simple query and condition",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{createCondition("A", "max", "gt", 42, 1, "5m", "now")}),
|
||||
[]dashAlertCondition{createCondition("A", "max", "gt", 42, 1, "5m", "now")}),
|
||||
createAlertWithCond(t, 2, 3, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{createCondition("A", "max", "gt", 42, 3, "5m", "now")}),
|
||||
[]dashAlertCondition{createCondition("A", "max", "gt", 42, 3, "5m", "now")}),
|
||||
},
|
||||
expected: map[int64][]*ngModels.AlertRule{
|
||||
int64(1): {
|
||||
genAlert(func(rule *ngModels.AlertRule) {
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "max", "gt", 42),
|
||||
}))
|
||||
}),
|
||||
@ -781,7 +794,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
rule.DashboardUID = pointer("dash3-2")
|
||||
rule.NamespaceUID = "folder6-2"
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds3-2", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "max", "gt", 42),
|
||||
}))
|
||||
}),
|
||||
@ -792,7 +805,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "multiple conditions",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
||||
createCondition("B", "max", "gt", 43, 2, "3m", "now"),
|
||||
createCondition("C", "min", "lt", 20, 2, "3m", "now"),
|
||||
@ -805,7 +818,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createAlertQuery("B", "ds2-1", "3m", "now"))
|
||||
rule.Data = append(rule.Data, createAlertQuery("C", "ds2-1", "3m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("D", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("D", []classicCondition{
|
||||
cond("A", "avg", "gt", 42),
|
||||
cond("B", "max", "gt", 43),
|
||||
cond("C", "min", "lt", 20),
|
||||
@ -818,7 +831,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "multiple conditions on same query with same timerange should not create multiple queries",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
||||
createCondition("A", "avg", "gt", 20, 1, "5m", "now"),
|
||||
}),
|
||||
@ -828,7 +841,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
genAlert(func(rule *ngModels.AlertRule) {
|
||||
rule.Condition = "B"
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "max", "gt", 42),
|
||||
cond("A", "avg", "gt", 20),
|
||||
}))
|
||||
@ -840,7 +853,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "multiple conditions on same query with different timeranges should create multiple queries",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
||||
createCondition("A", "avg", "gt", 20, 1, "3m", "now"),
|
||||
}),
|
||||
@ -851,7 +864,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
rule.Condition = "C"
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "3m", "now")) // Ordered by time range.
|
||||
rule.Data = append(rule.Data, createAlertQuery("B", "ds1-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("C", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("C", []classicCondition{
|
||||
cond("B", "max", "gt", 42),
|
||||
cond("A", "avg", "gt", 20),
|
||||
}))
|
||||
@ -863,7 +876,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "multiple conditions custom refIds",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("Q1", "avg", "gt", 42, 1, "5m", "now"),
|
||||
createCondition("Q2", "max", "gt", 43, 2, "3m", "now"),
|
||||
createCondition("Q3", "min", "lt", 20, 2, "3m", "now"),
|
||||
@ -873,7 +886,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
int64(1): {
|
||||
genAlert(func(rule *ngModels.AlertRule) {
|
||||
rule.Condition = "A"
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("A", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("A", []classicCondition{
|
||||
cond("Q1", "avg", "gt", 42),
|
||||
cond("Q2", "max", "gt", 43),
|
||||
cond("Q3", "min", "lt", 20),
|
||||
@ -889,7 +902,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "multiple conditions out of order refIds, queries should be sorted by refId and conditions should be in original order",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("B", "avg", "gt", 42, 1, "5m", "now"),
|
||||
createCondition("C", "max", "gt", 43, 2, "3m", "now"),
|
||||
createCondition("A", "min", "lt", 20, 2, "3m", "now"),
|
||||
@ -902,7 +915,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds2-1", "3m", "now"))
|
||||
rule.Data = append(rule.Data, createAlertQuery("B", "ds1-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createAlertQuery("C", "ds2-1", "3m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("D", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("D", []classicCondition{
|
||||
cond("B", "avg", "gt", 42),
|
||||
cond("C", "max", "gt", 43),
|
||||
cond("A", "min", "lt", 20),
|
||||
@ -915,7 +928,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "multiple conditions out of order with duplicate refIds",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("C", "avg", "gt", 42, 1, "5m", "now"),
|
||||
createCondition("C", "max", "gt", 43, 1, "3m", "now"),
|
||||
createCondition("B", "min", "lt", 20, 2, "5m", "now"),
|
||||
@ -930,7 +943,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
rule.Data = append(rule.Data, createAlertQuery("B", "ds2-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createAlertQuery("C", "ds1-1", "3m", "now"))
|
||||
rule.Data = append(rule.Data, createAlertQuery("D", "ds1-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("E", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("E", []classicCondition{
|
||||
cond("D", "avg", "gt", 42),
|
||||
cond("C", "max", "gt", 43),
|
||||
cond("B", "min", "lt", 20),
|
||||
@ -944,13 +957,13 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "alerts with unknown datasource id migrates with empty datasource uid",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{createCondition("A", "max", "gt", 42, 123, "5m", "now")}), // Unknown datasource id.
|
||||
[]dashAlertCondition{createCondition("A", "max", "gt", 42, 123, "5m", "now")}), // Unknown datasource id.
|
||||
},
|
||||
expected: map[int64][]*ngModels.AlertRule{
|
||||
int64(1): {
|
||||
genAlert(func(rule *ngModels.AlertRule) {
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "", "5m", "now")) // Empty datasource UID.
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "max", "gt", 42),
|
||||
}))
|
||||
}),
|
||||
@ -961,7 +974,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "alerts with unknown dashboard do not migrate",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 22, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
||||
}),
|
||||
},
|
||||
@ -973,7 +986,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "alerts with unknown org do not migrate",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 22, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
||||
}),
|
||||
},
|
||||
@ -985,7 +998,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "alerts in general folder migrate to existing general alerting",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 8, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
||||
}),
|
||||
},
|
||||
@ -995,7 +1008,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
rule.NamespaceUID = "General Alerting"
|
||||
rule.DashboardUID = pointer("dash-in-general-1")
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "avg", "gt", 42),
|
||||
}))
|
||||
}),
|
||||
@ -1006,7 +1019,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "alerts in general folder migrate to newly created general alerting if one doesn't exist",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 2, 9, 1, "alert1", nil, // Org 2 doesn't have general alerting folder.
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
createCondition("A", "avg", "gt", 42, 3, "5m", "now"),
|
||||
}),
|
||||
},
|
||||
@ -1022,18 +1035,26 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
rule.OrgID = 2
|
||||
rule.DashboardUID = pointer("dash-in-general-2")
|
||||
rule.Data = append(rule.Data, createAlertQuery("A", "ds3-2", "5m", "now"))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "avg", "gt", 42),
|
||||
}))
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed alert migration fails upgrade",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]dashAlertCondition{{}}),
|
||||
},
|
||||
expErrors: []string{"migrate alert 'alert1'"},
|
||||
},
|
||||
{
|
||||
name: "simple query with interval, calculates intervalMs using it as min interval",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
withQueryModel(
|
||||
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
||||
fmt.Sprintf(queryModel, "A", "1s"),
|
||||
@ -1044,7 +1065,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
int64(1): {
|
||||
genAlert(func(rule *ngModels.AlertRule) {
|
||||
rule.Data = append(rule.Data, createAlertQueryWithModel("A", "ds1-1", "5m", "now", fmt.Sprintf(newQueryModel, "1s", 1000, "A")))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "max", "gt", 42),
|
||||
}))
|
||||
}),
|
||||
@ -1055,7 +1076,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
name: "simple query with interval as variable, calculates intervalMs using default as min interval",
|
||||
alerts: []*models.Alert{
|
||||
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
||||
[]migrationStore.DashAlertCondition{
|
||||
[]dashAlertCondition{
|
||||
withQueryModel(
|
||||
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
||||
fmt.Sprintf(queryModel, "A", "$min_interval"),
|
||||
@ -1066,7 +1087,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
int64(1): {
|
||||
genAlert(func(rule *ngModels.AlertRule) {
|
||||
rule.Data = append(rule.Data, createAlertQueryWithModel("A", "ds1-1", "5m", "now", fmt.Sprintf(newQueryModel, "$min_interval", 1000, "A")))
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicConditionJSON{
|
||||
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
||||
cond("A", "max", "gt", 42),
|
||||
}))
|
||||
}),
|
||||
@ -1097,6 +1118,12 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
setupLegacyAlertsTables(t, x, nil, tt.alerts, folders, dashes)
|
||||
|
||||
err := service.Run(context.Background())
|
||||
if len(tt.expErrors) > 0 {
|
||||
for _, expErr := range tt.expErrors {
|
||||
require.ErrorContains(t, err, expErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for orgId, expected := range tt.expected {
|
||||
@ -1106,8 +1133,8 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
// Remove generated fields.
|
||||
require.NotEqual(t, r.Labels["rule_uid"], "")
|
||||
delete(r.Labels, "rule_uid")
|
||||
require.NotEqual(t, r.Annotations["__alertId__"], "")
|
||||
delete(r.Annotations, "__alertId__")
|
||||
require.NotEqual(t, r.Annotations[ngModels.MigratedAlertIdAnnotation], "")
|
||||
delete(r.Annotations, ngModels.MigratedAlertIdAnnotation)
|
||||
|
||||
// If folder is created, we check if separately
|
||||
if tt.expectedFolder != nil {
|
||||
@ -1141,6 +1168,7 @@ const (
|
||||
emailSettings = `{"addresses": "test"}`
|
||||
slackSettings = `{"recipient": "test", "token": "test"}`
|
||||
opsgenieSettings = `{"apiKey": "test"}`
|
||||
brokenSettings = `[{"unknown": 1.5}]`
|
||||
)
|
||||
|
||||
var (
|
||||
@ -1180,16 +1208,16 @@ func createAlertNotification(t *testing.T, orgId int64, uid string, channelType
|
||||
return createAlertNotificationWithReminder(t, orgId, uid, channelType, settings, defaultChannel, false, time.Duration(0))
|
||||
}
|
||||
|
||||
func withQueryModel(base migrationStore.DashAlertCondition, model string) migrationStore.DashAlertCondition {
|
||||
func withQueryModel(base dashAlertCondition, model string) dashAlertCondition {
|
||||
base.Query.Model = []byte(model)
|
||||
return base
|
||||
}
|
||||
|
||||
var queryModel = `{"datasource":{"type":"prometheus","uid":"gdev-prometheus"},"expr":"up{job=\"fake-data-gen\"}","instant":false,"refId":"%s","interval":"%s"}`
|
||||
|
||||
func createCondition(refId string, reducer string, evalType string, thresh float64, datasourceId int64, from string, to string) migrationStore.DashAlertCondition {
|
||||
return migrationStore.DashAlertCondition{
|
||||
Evaluator: migrationStore.ConditionEvalJSON{
|
||||
func createCondition(refId string, reducer string, evalType string, thresh float64, datasourceId int64, from string, to string) dashAlertCondition {
|
||||
return dashAlertCondition{
|
||||
Evaluator: evaluator{
|
||||
Params: []float64{thresh},
|
||||
Type: evalType,
|
||||
},
|
||||
@ -1217,20 +1245,18 @@ func createCondition(refId string, reducer string, evalType string, thresh float
|
||||
|
||||
// createAlert creates a legacy alert rule for inserting into the test database.
|
||||
func createAlert(t *testing.T, orgId int, dashboardId int, panelsId int, name string, notifierUids []string) *models.Alert {
|
||||
return createAlertWithCond(t, orgId, dashboardId, panelsId, name, notifierUids, []migrationStore.DashAlertCondition{})
|
||||
return createAlertWithCond(t, orgId, dashboardId, panelsId, name, notifierUids, []dashAlertCondition{})
|
||||
}
|
||||
|
||||
// createAlert creates a legacy alert rule for inserting into the test database.
|
||||
func createAlertWithCond(t *testing.T, orgId int, dashboardId int, panelsId int, name string, notifierUids []string, cond []migrationStore.DashAlertCondition) *models.Alert {
|
||||
func createAlertWithCond(t *testing.T, orgId int, dashboardId int, panelsId int, name string, notifierUids []string, cond []dashAlertCondition) *models.Alert {
|
||||
t.Helper()
|
||||
|
||||
var settings = simplejson.New()
|
||||
if len(notifierUids) != 0 {
|
||||
notifiers := make([]any, 0)
|
||||
for _, n := range notifierUids {
|
||||
notifiers = append(notifiers, struct {
|
||||
Uid string
|
||||
}{Uid: n})
|
||||
notifiers = append(notifiers, notificationKey{UID: n})
|
||||
}
|
||||
|
||||
settings.Set("notifications", notifiers)
|
||||
|
@ -5,10 +5,10 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -25,6 +25,7 @@ type OrgMigration struct {
|
||||
orgID int64
|
||||
silences []*pb.MeshSilence
|
||||
titleDeduplicatorForFolder func(folderUID string) *migmodels.Deduplicator
|
||||
channelCache *ChannelCache
|
||||
|
||||
// Migrated folder for a dashboard based on permissions. Parent Folder ID -> unique dashboard permission -> custom folder.
|
||||
permissionsMap map[int64]map[permissionHash]*folder.Folder
|
||||
@ -53,6 +54,7 @@ func (ms *migrationService) newOrgMigration(orgID int64) *OrgMigration {
|
||||
}
|
||||
return titlededuplicatorPerFolder[folderUID]
|
||||
},
|
||||
channelCache: &ChannelCache{cache: make(map[any]*legacymodels.AlertNotification)},
|
||||
|
||||
permissionsMap: make(map[int64]map[permissionHash]*folder.Folder),
|
||||
folderCache: make(map[int64]*folder.Folder),
|
||||
@ -65,7 +67,24 @@ func (ms *migrationService) newOrgMigration(orgID int64) *OrgMigration {
|
||||
}
|
||||
}
|
||||
|
||||
type AlertPair struct {
|
||||
AlertRule *models.AlertRule
|
||||
DashAlert *migrationStore.DashAlert
|
||||
// ChannelCache caches channels by ID and UID.
|
||||
type ChannelCache struct {
|
||||
cache map[any]*legacymodels.AlertNotification
|
||||
}
|
||||
|
||||
func (c *ChannelCache) LoadChannels(channels []*legacymodels.AlertNotification) {
|
||||
for _, channel := range channels {
|
||||
c.cache[channel.ID] = channel
|
||||
c.cache[channel.UID] = channel
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ChannelCache) GetChannelByID(id int64) (*legacymodels.AlertNotification, bool) {
|
||||
channel, ok := c.cache[id]
|
||||
return channel, ok
|
||||
}
|
||||
|
||||
func (c *ChannelCache) GetChannelByUID(uid string) (*legacymodels.AlertNotification, bool) {
|
||||
channel, ok := c.cache[uid]
|
||||
return channel, ok
|
||||
}
|
||||
|
79
pkg/services/ngalert/migration/models/alertmanager.go
Normal file
79
pkg/services/ngalert/migration/models/alertmanager.go
Normal file
@ -0,0 +1,79 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
apiModels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
// Alertmanager is a helper struct for creating migrated alertmanager configs.
|
||||
type Alertmanager struct {
|
||||
Config *apiModels.PostableUserConfig
|
||||
legacyRoute *apiModels.Route
|
||||
}
|
||||
|
||||
// NewAlertmanager creates a new Alertmanager.
|
||||
func NewAlertmanager() *Alertmanager {
|
||||
c, r := createBaseConfig()
|
||||
return &Alertmanager{
|
||||
Config: c,
|
||||
legacyRoute: r,
|
||||
}
|
||||
}
|
||||
|
||||
// AddRoute adds a route to the alertmanager config.
|
||||
func (am *Alertmanager) AddRoute(route *apiModels.Route) {
|
||||
am.legacyRoute.Routes = append(am.legacyRoute.Routes, route)
|
||||
}
|
||||
|
||||
// AddReceiver adds a receiver to the alertmanager config.
|
||||
func (am *Alertmanager) AddReceiver(recv *apiModels.PostableApiReceiver) {
|
||||
am.Config.AlertmanagerConfig.Receivers = append(am.Config.AlertmanagerConfig.Receivers, recv)
|
||||
}
|
||||
|
||||
// createBaseConfig creates an alertmanager config with the root-level route, default receiver, and nested route
|
||||
// for migrated channels.
|
||||
func createBaseConfig() (*apiModels.PostableUserConfig, *apiModels.Route) {
|
||||
defaultRoute, nestedRoute := createDefaultRoute()
|
||||
return &apiModels.PostableUserConfig{
|
||||
AlertmanagerConfig: apiModels.PostableApiAlertingConfig{
|
||||
Receivers: []*apiModels.PostableApiReceiver{
|
||||
{
|
||||
Receiver: config.Receiver{
|
||||
Name: "autogen-contact-point-default",
|
||||
},
|
||||
PostableGrafanaReceivers: apiModels.PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: []*apiModels.PostableGrafanaReceiver{},
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: apiModels.Config{
|
||||
Route: defaultRoute,
|
||||
},
|
||||
},
|
||||
}, nestedRoute
|
||||
}
|
||||
|
||||
// createDefaultRoute creates a default root-level route and associated nested route that will contain all the migrated channels.
|
||||
func createDefaultRoute() (*apiModels.Route, *apiModels.Route) {
|
||||
nestedRoute := createNestedLegacyRoute()
|
||||
return &apiModels.Route{
|
||||
Receiver: "autogen-contact-point-default",
|
||||
Routes: []*apiModels.Route{nestedRoute},
|
||||
GroupByStr: []string{ngmodels.FolderTitleLabel, model.AlertNameLabel}, // To keep parity with pre-migration notifications.
|
||||
RepeatInterval: nil,
|
||||
}, nestedRoute
|
||||
}
|
||||
|
||||
// createNestedLegacyRoute creates a nested route that will contain all the migrated channels.
|
||||
// This route is matched on the UseLegacyChannelsLabel and mostly exists to keep the migrated channels separate and organized.
|
||||
func createNestedLegacyRoute() *apiModels.Route {
|
||||
mat, _ := labels.NewMatcher(labels.MatchEqual, ngmodels.MigratedUseLegacyChannelsLabel, "true")
|
||||
return &apiModels.Route{
|
||||
ObjectMatchers: apiModels.ObjectMatchers{mat},
|
||||
Continue: true,
|
||||
}
|
||||
}
|
@ -109,7 +109,7 @@ func (om *OrgMigration) getOrCreateMigratedFolder(ctx context.Context, l log.Log
|
||||
if !ok {
|
||||
permissionsToFolder = make(map[permissionHash]*folder.Folder)
|
||||
// nolint:staticcheck
|
||||
om.permissionsMap[dash.FolderID] = permissionsToFolder
|
||||
om.permissionsMap[parentFolder.ID] = permissionsToFolder
|
||||
|
||||
folderPerms, err := om.getFolderPermissions(ctx, parentFolder)
|
||||
if err != nil {
|
||||
|
@ -69,7 +69,7 @@ func TestDashAlertPermissionMigration(t *testing.T) {
|
||||
"__dashboardUid__": dashboardUID,
|
||||
"__panelId__": "1",
|
||||
},
|
||||
Labels: map[string]string{},
|
||||
Labels: map[string]string{ngModels.MigratedUseLegacyChannelsLabel: "true"},
|
||||
IsPaused: false,
|
||||
}
|
||||
if len(mutators) > 0 {
|
||||
@ -685,8 +685,8 @@ func TestDashAlertPermissionMigration(t *testing.T) {
|
||||
// Remove generated fields.
|
||||
require.NotEqual(t, r.Labels["rule_uid"], "")
|
||||
delete(r.Labels, "rule_uid")
|
||||
require.NotEqual(t, r.Annotations["__alertId__"], "")
|
||||
delete(r.Annotations, "__alertId__")
|
||||
require.NotEqual(t, r.Annotations[ngModels.MigratedAlertIdAnnotation], "")
|
||||
delete(r.Annotations, ngModels.MigratedAlertIdAnnotation)
|
||||
|
||||
folder := getDashboard(t, x, orgId, r.NamespaceUID)
|
||||
rperms, err := service.migrationStore.GetFolderPermissions(context.Background(), getMigrationUser(orgId), folder.UID)
|
||||
|
@ -43,7 +43,7 @@ type Store interface {
|
||||
|
||||
GetNotificationChannels(ctx context.Context, orgID int64) ([]*legacymodels.AlertNotification, error)
|
||||
|
||||
GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*DashAlert, int, error)
|
||||
GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*legacymodels.Alert, int, error)
|
||||
|
||||
GetDashboardPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error)
|
||||
GetFolderPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error)
|
||||
@ -253,6 +253,7 @@ func (ms *migrationStore) InsertAlertRules(ctx context.Context, rules ...models.
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAlertmanagerConfiguration saves the alertmanager configuration for the given org.
|
||||
func (ms *migrationStore) SaveAlertmanagerConfiguration(ctx context.Context, orgID int64, amConfig *apimodels.PostableUserConfig) error {
|
||||
rawAmConfig, err := json.Marshal(amConfig)
|
||||
if err != nil {
|
||||
@ -407,15 +408,18 @@ func (ms *migrationStore) DeleteFolders(ctx context.Context, orgID int64, uids .
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDashboard returns a single dashboard for the given org and dashboard id.
|
||||
func (ms *migrationStore) GetDashboard(ctx context.Context, orgID int64, id int64) (*dashboards.Dashboard, error) {
|
||||
return ms.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{ID: id, OrgID: orgID})
|
||||
}
|
||||
|
||||
// GetAllOrgs returns all orgs.
|
||||
func (ms *migrationStore) GetAllOrgs(ctx context.Context) ([]*org.OrgDTO, error) {
|
||||
orgQuery := &org.SearchOrgsQuery{}
|
||||
return ms.orgService.Search(ctx, orgQuery)
|
||||
}
|
||||
|
||||
// GetDatasource returns a single datasource for the given org and datasource id.
|
||||
func (ms *migrationStore) GetDatasource(ctx context.Context, datasourceID int64, user identity.Requester) (*datasources.DataSource, error) {
|
||||
return ms.dataSourceCache.GetDatasource(ctx, datasourceID, user, false)
|
||||
}
|
||||
@ -428,35 +432,20 @@ func (ms *migrationStore) GetNotificationChannels(ctx context.Context, orgID int
|
||||
}
|
||||
|
||||
// GetOrgDashboardAlerts loads all legacy dashboard alerts for the given org mapped by dashboard id.
|
||||
func (ms *migrationStore) GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*DashAlert, int, error) {
|
||||
var alerts []legacymodels.Alert
|
||||
func (ms *migrationStore) GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*legacymodels.Alert, int, error) {
|
||||
var dashAlerts []*legacymodels.Alert
|
||||
err := ms.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL("select * from alert WHERE org_id = ? AND dashboard_id IN (SELECT id from dashboard)", orgID).Find(&alerts)
|
||||
return sess.SQL("select * from alert WHERE org_id = ? AND dashboard_id IN (SELECT id from dashboard)", orgID).Find(&dashAlerts)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
mappedAlerts := make(map[int64][]*DashAlert)
|
||||
for i := range alerts {
|
||||
alert := alerts[i]
|
||||
|
||||
rawSettings, err := json.Marshal(alert.Settings)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("get settings for alert rule ID:%d, name:'%s', orgID:%d: %w", alert.ID, alert.Name, alert.OrgID, err)
|
||||
}
|
||||
var parsedSettings DashAlertSettings
|
||||
err = json.Unmarshal(rawSettings, &parsedSettings)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("parse settings for alert rule ID:%d, name:'%s', orgID:%d: %w", alert.ID, alert.Name, alert.OrgID, err)
|
||||
}
|
||||
|
||||
mappedAlerts[alert.DashboardID] = append(mappedAlerts[alert.DashboardID], &DashAlert{
|
||||
Alert: &alerts[i],
|
||||
ParsedSettings: &parsedSettings,
|
||||
})
|
||||
mappedAlerts := make(map[int64][]*legacymodels.Alert)
|
||||
for _, alert := range dashAlerts {
|
||||
mappedAlerts[alert.DashboardID] = append(mappedAlerts[alert.DashboardID], alert)
|
||||
}
|
||||
return mappedAlerts, len(alerts), nil
|
||||
return mappedAlerts, len(dashAlerts), nil
|
||||
}
|
||||
|
||||
func (ms *migrationStore) GetDashboardPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
|
||||
|
@ -1,58 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
)
|
||||
|
||||
// uidOrID for both uid and ID, primarily used for mapping legacy channel to migrated receiver.
|
||||
type UidOrID any
|
||||
|
||||
type DashAlert struct {
|
||||
*legacymodels.Alert
|
||||
ParsedSettings *DashAlertSettings
|
||||
}
|
||||
|
||||
// dashAlertSettings is a type for the JSON that is in the settings field of
|
||||
// the alert table.
|
||||
type DashAlertSettings struct {
|
||||
NoDataState string `json:"noDataState"`
|
||||
ExecutionErrorState string `json:"executionErrorState"`
|
||||
Conditions []DashAlertCondition `json:"conditions"`
|
||||
AlertRuleTags any `json:"alertRuleTags"`
|
||||
Notifications []DashAlertNot `json:"notifications"`
|
||||
}
|
||||
|
||||
// dashAlertNot is the object that represents the Notifications array in
|
||||
// dashAlertSettings
|
||||
type DashAlertNot struct {
|
||||
UID string `json:"uid,omitempty"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// dashAlertingConditionJSON is like classic.ClassicConditionJSON except that it
|
||||
// includes the model property with the query.
|
||||
type DashAlertCondition struct {
|
||||
Evaluator ConditionEvalJSON `json:"evaluator"`
|
||||
|
||||
Operator struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"operator"`
|
||||
|
||||
Query struct {
|
||||
Params []string `json:"params"`
|
||||
DatasourceID int64 `json:"datasourceId"`
|
||||
Model json.RawMessage
|
||||
} `json:"query"`
|
||||
|
||||
Reducer struct {
|
||||
// Params []any `json:"params"` (Unused)
|
||||
Type string `json:"type"`
|
||||
}
|
||||
}
|
||||
|
||||
type ConditionEvalJSON struct {
|
||||
Params []float64 `json:"params"`
|
||||
Type string `json:"type"` // e.g. "gt"
|
||||
}
|
@ -4,13 +4,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
func (om *OrgMigration) migrateAlerts(ctx context.Context, alerts []*migrationStore.DashAlert, info migmodels.DashboardUpgradeInfo) ([]*AlertPair, error) {
|
||||
func (om *OrgMigration) migrateAlerts(ctx context.Context, alerts []*legacymodels.Alert, info migmodels.DashboardUpgradeInfo) ([]models.AlertRule, error) {
|
||||
log := om.log.New(
|
||||
"dashboardUid", info.DashboardUID,
|
||||
"dashboardName", info.DashboardName,
|
||||
@ -18,97 +17,91 @@ func (om *OrgMigration) migrateAlerts(ctx context.Context, alerts []*migrationSt
|
||||
"newFolderNane", info.NewFolderName,
|
||||
)
|
||||
|
||||
pairs := make([]*AlertPair, 0, len(alerts))
|
||||
rules := make([]models.AlertRule, 0, len(alerts))
|
||||
for _, da := range alerts {
|
||||
al := log.New("ruleID", da.ID, "ruleName", da.Name)
|
||||
alertRule, err := om.migrateAlert(ctx, al, da, info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrate alert: %w", err)
|
||||
return nil, fmt.Errorf("migrate alert '%s': %w", da.Name, err)
|
||||
}
|
||||
pairs = append(pairs, &AlertPair{AlertRule: alertRule, DashAlert: da})
|
||||
rules = append(rules, *alertRule)
|
||||
}
|
||||
|
||||
return pairs, nil
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (om *OrgMigration) migrateDashboard(ctx context.Context, dashID int64, alerts []*migrationStore.DashAlert) ([]*AlertPair, error) {
|
||||
func (om *OrgMigration) migrateDashboard(ctx context.Context, dashID int64, alerts []*legacymodels.Alert) ([]models.AlertRule, error) {
|
||||
info, err := om.migratedFolder(ctx, om.log, dashID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get or create migrated folder: %w", err)
|
||||
}
|
||||
pairs, err := om.migrateAlerts(ctx, alerts, *info)
|
||||
rules, err := om.migrateAlerts(ctx, alerts, *info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrate and save alerts: %w", err)
|
||||
}
|
||||
|
||||
return pairs, nil
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (om *OrgMigration) migrateOrgAlerts(ctx context.Context) ([]*AlertPair, error) {
|
||||
func (om *OrgMigration) migrateOrgAlerts(ctx context.Context) error {
|
||||
mappedAlerts, cnt, err := om.migrationStore.GetOrgDashboardAlerts(ctx, om.orgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load alerts: %w", err)
|
||||
return fmt.Errorf("load alerts: %w", err)
|
||||
}
|
||||
om.log.Info("Alerts found to migrate", "alerts", cnt)
|
||||
|
||||
pairs := make([]*AlertPair, 0, cnt)
|
||||
for dashID, alerts := range mappedAlerts {
|
||||
dashPairs, err := om.migrateDashboard(ctx, dashID, alerts)
|
||||
rules, err := om.migrateDashboard(ctx, dashID, alerts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrate and save dashboard '%d': %w", dashID, err)
|
||||
return fmt.Errorf("migrate and save dashboard '%d': %w", dashID, err)
|
||||
}
|
||||
|
||||
if len(rules) > 0 {
|
||||
om.log.Debug("Inserting migrated alert rules", "count", len(rules))
|
||||
err := om.migrationStore.InsertAlertRules(ctx, rules...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert alert rules: %w", err)
|
||||
}
|
||||
}
|
||||
pairs = append(pairs, dashPairs...)
|
||||
}
|
||||
return pairs, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (om *OrgMigration) migrateOrgChannels(ctx context.Context, pairs []*AlertPair) (*apimodels.PostableUserConfig, error) {
|
||||
func (om *OrgMigration) migrateOrgChannels(ctx context.Context) (*migmodels.Alertmanager, error) {
|
||||
channels, err := om.migrationStore.GetNotificationChannels(ctx, om.orgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load notification channels: %w", err)
|
||||
}
|
||||
|
||||
amConfig, err := om.migrateChannels(channels, pairs)
|
||||
// Cache for later use by alerts
|
||||
om.channelCache.LoadChannels(channels)
|
||||
|
||||
amConfig, err := om.migrateChannels(channels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return amConfig, nil
|
||||
}
|
||||
|
||||
func (om *OrgMigration) migrateOrg(ctx context.Context) error {
|
||||
om.log.Info("Migrating alerts for organisation")
|
||||
|
||||
pairs, err := om.migrateOrgAlerts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate alerts: %w", err)
|
||||
}
|
||||
|
||||
// This must happen before we insert the rules into the database because it modifies the alert labels. This will
|
||||
// be changed in the future when we improve how notification policies are created.
|
||||
amConfig, err := om.migrateOrgChannels(ctx, pairs)
|
||||
amConfig, err := om.migrateOrgChannels(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate channels: %w", err)
|
||||
}
|
||||
|
||||
err = om.migrateOrgAlerts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate alerts: %w", err)
|
||||
}
|
||||
|
||||
if err := om.writeSilencesFile(); err != nil {
|
||||
return fmt.Errorf("write silence file for org %d: %w", om.orgID, err)
|
||||
}
|
||||
|
||||
if len(pairs) > 0 {
|
||||
om.log.Debug("Inserting migrated alert rules", "count", len(pairs))
|
||||
rules := make([]models.AlertRule, 0, len(pairs))
|
||||
for _, p := range pairs {
|
||||
rules = append(rules, *p.AlertRule)
|
||||
}
|
||||
err := om.migrationStore.InsertAlertRules(ctx, rules...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert alert rules: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if amConfig != nil {
|
||||
if err := om.migrationStore.SaveAlertmanagerConfiguration(ctx, om.orgID, amConfig); err != nil {
|
||||
if err := om.migrationStore.SaveAlertmanagerConfiguration(ctx, om.orgID, amConfig.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -105,6 +105,17 @@ const (
|
||||
|
||||
// StateReasonAnnotation is the name of the annotation that explains the difference between evaluation state and alert state (i.e. changing state when NoData or Error).
|
||||
StateReasonAnnotation = GrafanaReservedLabelPrefix + "state_reason"
|
||||
|
||||
// MigratedLabelPrefix is a label prefix for all labels created during legacy migration.
|
||||
MigratedLabelPrefix = "__legacy_"
|
||||
// MigratedUseLegacyChannelsLabel is created during legacy migration to route to separate nested policies for migrated channels.
|
||||
MigratedUseLegacyChannelsLabel = MigratedLabelPrefix + "use_channels__"
|
||||
// MigratedContactLabelPrefix is created during legacy migration to route a migrated alert rule to a specific migrated channel.
|
||||
MigratedContactLabelPrefix = MigratedLabelPrefix + "c_"
|
||||
// MigratedAlertIdAnnotation is created during legacy migration to store the ID of the migrated legacy alert rule.
|
||||
MigratedAlertIdAnnotation = "__alertId__"
|
||||
// MigratedMessageAnnotation is created during legacy migration to store the migrated alert message.
|
||||
MigratedMessageAnnotation = "message"
|
||||
)
|
||||
|
||||
const (
|
||||
|
Loading…
Reference in New Issue
Block a user