Files
grafana/pkg/services/ngalert/migration/service_test.go
T
Matthew Jacobson aa03b8f8a7 Alerting: Guided legacy alerting upgrade dry-run (#80071)
This PR has two steps that together create a functional dry-run capability for the migration.

By enabling the feature flag alertingPreviewUpgrade when on legacy alerting it will:
    a. Allow all Grafana Alerting background services except for the scheduler to start (multiorg alertmanager, state manager, routes, …).
    b. Allow the UI to show Grafana Alerting pages alongside legacy ones (with appropriate in-app warnings that UA is not actually running).
    c. Show a new “Alerting Upgrade” page and register associated /api/v1/upgrade endpoints that will allow the user to upgrade their organization live without restart and present a summary of the upgrade in a table.
2024-01-05 18:19:12 -05:00

2135 lines
75 KiB
Go

package migration
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
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/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/infra/db"
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/setting"
)
// TestServiceRevert tests migration revert.
func TestServiceRevert(t *testing.T) {
alerts := []*legacymodels.Alert{
createAlert(t, 1, 1, 1, "alert1", []string{"notifier1"}),
}
channels := []*legacymodels.AlertNotification{
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
}
dashes := []*dashboards.Dashboard{
createDashboard(t, 1, 1, "dash1-1", 5, nil),
createDashboard(t, 2, 1, "dash2-1", 5, nil),
createDashboard(t, 8, 1, "dash-in-general-1", 0, nil),
}
folders := []*dashboards.Dashboard{
createFolder(t, 5, 1, "folder5-1"),
}
t.Run("revert deletes UA resources", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
x := sqlStore.GetEngine()
setupLegacyAlertsTables(t, x, channels, alerts, folders, dashes)
dashCount, err := x.Table("dashboard").Count(&dashboards.Dashboard{})
require.NoError(t, err)
require.Equal(t, int64(4), dashCount)
// Run migration.
ctx := context.Background()
cfg := &setting.Cfg{
UnifiedAlerting: setting.UnifiedAlertingSettings{
Enabled: pointer(true),
},
}
service := NewTestMigrationService(t, sqlStore, cfg)
err = service.migrationStore.SetCurrentAlertingType(ctx, migrationStore.Legacy)
require.NoError(t, err)
require.NoError(t, service.Run(ctx))
// Verify migration was run.
checkAlertingType(t, ctx, service, migrationStore.UnifiedAlerting)
checkMigrationStatus(t, ctx, service, 1, true)
// Currently, we fill in some random data for tables that aren't populated during migration.
_, err = x.Table("ngalert_configuration").Insert(models.AdminConfiguration{OrgID: 1})
require.NoError(t, err)
_, err = x.Table("alert_instance").Insert(models.AlertInstance{
AlertInstanceKey: models.AlertInstanceKey{
RuleOrgID: 1,
RuleUID: "alert1",
LabelsHash: "",
},
CurrentState: models.InstanceStateNormal,
CurrentStateSince: time.Now(),
CurrentStateEnd: time.Now(),
LastEvalTime: time.Now(),
})
require.NoError(t, err)
// Verify various UA resources exist
tables := [][2]string{
{"alert_rule", "org_id"},
{"alert_rule_version", "rule_org_id"},
{"alert_configuration", "org_id"},
{"ngalert_configuration", "org_id"},
{"alert_instance", "rule_org_id"},
}
for _, table := range tables {
count, err := x.Table(table[0]).Where(fmt.Sprintf("%s=?", table[1]), 1).Count()
require.NoErrorf(t, err, "table %s error", table[0])
require.True(t, count > 0, "table %s should have at least one row", table[0])
}
// Revert migration.
err = service.migrationStore.RevertAllOrgs(context.Background())
require.NoError(t, err)
// Verify revert was run.
checkAlertingType(t, ctx, service, migrationStore.Legacy)
checkMigrationStatus(t, ctx, service, 1, false)
// Verify various UA resources are gone
for _, table := range tables {
count, err := x.Table(table[0]).Where(fmt.Sprintf("%s=?", table[1]), 1).Count()
require.NoErrorf(t, err, "table %s error", table[0])
require.Equal(t, int64(0), count, "table %s should have no rows", table[0])
}
})
t.Run("revert deletes folders created during migration", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
x := sqlStore.GetEngine()
alerts = []*legacymodels.Alert{
createAlert(t, 1, 8, 1, "alert1", []string{"notifier1"}),
}
setupLegacyAlertsTables(t, x, channels, alerts, folders, dashes)
dashCount, err := x.Table("dashboard").Count(&dashboards.Dashboard{})
require.NoError(t, err)
require.Equal(t, int64(4), dashCount)
// Run migration.
ctx := context.Background()
cfg := &setting.Cfg{
UnifiedAlerting: setting.UnifiedAlertingSettings{
Enabled: pointer(true),
},
}
service := NewTestMigrationService(t, sqlStore, cfg)
err = service.migrationStore.SetCurrentAlertingType(ctx, migrationStore.Legacy)
require.NoError(t, err)
require.NoError(t, service.Run(ctx))
// Verify migration was run.
checkAlertingType(t, ctx, service, migrationStore.UnifiedAlerting)
checkMigrationStatus(t, ctx, service, 1, true)
// Verify we created some folders.
newDashCount, err := x.Table("dashboard").Count(&dashboards.Dashboard{})
require.NoError(t, err)
require.Truef(t, newDashCount > dashCount, "newDashCount: %d should be greater than dashCount: %d", newDashCount, dashCount)
// Check that dashboards and folders from before migration still exist.
require.NotNil(t, getDashboard(t, x, 1, "dash1-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash2-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash-in-general-1"))
state, err := service.migrationStore.GetOrgMigrationState(ctx, 1)
require.NoError(t, err)
// Verify list of created folders.
require.NotEmpty(t, state.CreatedFolders)
for _, uid := range state.CreatedFolders {
require.NotNil(t, getDashboard(t, x, 1, uid))
}
// Revert migration.
err = service.migrationStore.RevertAllOrgs(context.Background())
require.NoError(t, err)
// Verify revert was run. Should only set migration status for org.
checkAlertingType(t, ctx, service, migrationStore.Legacy)
checkMigrationStatus(t, ctx, service, 1, false)
// Verify we are back to the original count.
newDashCount, err = x.Table("dashboard").Count(&dashboards.Dashboard{})
require.NoError(t, err)
require.Equalf(t, dashCount, newDashCount, "newDashCount: %d should be equal to dashCount: %d after revert", newDashCount, dashCount)
// Check that dashboards and folders from before migration still exist.
require.NotNil(t, getDashboard(t, x, 1, "dash1-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash2-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash-in-general-1"))
// Check that folders created during migration are gone.
for _, uid := range state.CreatedFolders {
require.Nil(t, getDashboard(t, x, 1, uid))
}
})
t.Run("revert skips migrated folders that are not empty", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
x := sqlStore.GetEngine()
alerts = []*legacymodels.Alert{
createAlert(t, 1, 8, 1, "alert1", []string{"notifier1"}),
}
setupLegacyAlertsTables(t, x, channels, alerts, folders, dashes)
dashCount, err := x.Table("dashboard").Count(&dashboards.Dashboard{})
require.NoError(t, err)
require.Equal(t, int64(4), dashCount)
// Run migration.
ctx := context.Background()
cfg := &setting.Cfg{
UnifiedAlerting: setting.UnifiedAlertingSettings{
Enabled: pointer(true),
Upgrade: setting.UnifiedAlertingUpgradeSettings{},
},
}
service := NewTestMigrationService(t, sqlStore, cfg)
err = service.migrationStore.SetCurrentAlertingType(ctx, migrationStore.Legacy)
require.NoError(t, err)
require.NoError(t, service.Run(ctx))
// Verify migration was run.
checkAlertingType(t, ctx, service, migrationStore.UnifiedAlerting)
checkMigrationStatus(t, ctx, service, 1, true)
// Verify we created some folders.
newDashCount, err := x.Table("dashboard").Count(&dashboards.Dashboard{OrgID: 1})
require.NoError(t, err)
require.Truef(t, newDashCount > dashCount, "newDashCount: %d should be greater than dashCount: %d", newDashCount, dashCount)
// Check that dashboards and folders from before migration still exist.
require.NotNil(t, getDashboard(t, x, 1, "dash1-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash2-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash-in-general-1"))
state, err := service.migrationStore.GetOrgMigrationState(ctx, 1)
require.NoError(t, err)
// Verify list of created folders.
require.NotEmpty(t, state.CreatedFolders)
var generalAlertingFolder *dashboards.Dashboard
for _, uid := range state.CreatedFolders {
f := getDashboard(t, x, 1, uid)
require.NotNil(t, f)
if f.Slug == "general-alerting" {
generalAlertingFolder = f
}
}
require.NotNil(t, generalAlertingFolder)
// Create dashboard in general alerting.
newDashes := []*dashboards.Dashboard{
createDashboard(t, 99, 1, "dash-in-general-alerting-1", generalAlertingFolder.ID, nil),
}
_, err = x.Insert(newDashes)
require.NoError(t, err)
newF := getDashboard(t, x, 1, "dash-in-general-alerting-1")
require.NotNil(t, newF)
// Revert migration.
err = service.migrationStore.RevertAllOrgs(context.Background())
require.NoError(t, err)
// Verify revert was run. Should only set migration status for org.
checkAlertingType(t, ctx, service, migrationStore.Legacy)
checkMigrationStatus(t, ctx, service, 1, false)
// Verify we are back to the original count + 2.
newDashCount, err = x.Table("dashboard").Count(&dashboards.Dashboard{OrgID: 1})
require.NoError(t, err)
require.Equalf(t, dashCount+2, newDashCount, "newDashCount: %d should be equal to dashCount + 2: %d after revert", newDashCount, dashCount)
// Check that dashboards and folders from before migration still exist.
require.NotNil(t, getDashboard(t, x, 1, "dash1-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash2-1"))
require.NotNil(t, getDashboard(t, x, 1, "dash-in-general-1"))
// Check that the general alerting folder still exists.
require.NotNil(t, getDashboard(t, x, 1, generalAlertingFolder.UID))
// Check that the new dashboard in general alerting folder still exists.
require.NotNil(t, getDashboard(t, x, 1, "dash-in-general-alerting-1"))
// Check that other folders created during migration are gone.
for _, uid := range state.CreatedFolders {
if uid == generalAlertingFolder.UID {
continue
}
require.Nil(t, getDashboard(t, x, 1, uid))
}
})
t.Run("CleanUpgrade story", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
x := sqlStore.GetEngine()
setupLegacyAlertsTables(t, x, channels, alerts, folders, dashes)
ctx := context.Background()
cfg := &setting.Cfg{
UnifiedAlerting: setting.UnifiedAlertingSettings{
Enabled: pointer(true),
},
}
service := NewTestMigrationService(t, sqlStore, cfg)
checkAlertingType(t, ctx, service, migrationStore.Legacy)
checkMigrationStatus(t, ctx, service, 1, false)
checkAlertRulesCount(t, x, 1, 0)
// Enable UA.
// First run should migrate org.
require.NoError(t, service.Run(ctx))
checkAlertingType(t, ctx, service, migrationStore.UnifiedAlerting)
checkMigrationStatus(t, ctx, service, 1, true)
checkAlertRulesCount(t, x, 1, 1)
// Disable UA.
// This run should just set migration status to false.
service.cfg.UnifiedAlerting.Enabled = pointer(false)
require.NoError(t, service.Run(ctx))
checkAlertingType(t, ctx, service, migrationStore.Legacy)
checkMigrationStatus(t, ctx, service, 1, true)
checkAlertRulesCount(t, x, 1, 1)
// Add another alert.
// Enable UA without clean flag.
// This run should not remigrate org, new alert is not migrated.
_, alertErr := x.Insert(createAlert(t, 1, 1, 2, "alert2", []string{"notifier1"}))
require.NoError(t, alertErr)
service.cfg.UnifiedAlerting.Enabled = pointer(true)
require.NoError(t, service.Run(ctx))
checkAlertingType(t, ctx, service, migrationStore.UnifiedAlerting)
checkMigrationStatus(t, ctx, service, 1, true)
checkAlertRulesCount(t, x, 1, 1) // Still 1
// Disable UA with clean flag.
// This run should not revert UA data.
service.cfg.UnifiedAlerting.Enabled = pointer(false)
service.cfg.UnifiedAlerting.Upgrade.CleanUpgrade = true
require.NoError(t, service.Run(ctx))
checkAlertingType(t, ctx, service, migrationStore.Legacy)
checkMigrationStatus(t, ctx, service, 1, true)
checkAlertRulesCount(t, x, 1, 1) // Still 1
// Enable UA with clean flag.
// This run should revert and remigrate org, new alert is migrated.
service.cfg.UnifiedAlerting.Enabled = pointer(true)
require.NoError(t, service.Run(ctx))
checkAlertingType(t, ctx, service, migrationStore.UnifiedAlerting)
checkMigrationStatus(t, ctx, service, 1, true)
checkAlertRulesCount(t, x, 1, 2) // Now we have 2
// The following tests ForceMigration which is deprecated and will be removed in v11.
service.cfg.UnifiedAlerting.Upgrade.CleanUpgrade = false
// Disable UA with force flag.
// This run should not revert UA data.
service.cfg.UnifiedAlerting.Enabled = pointer(false)
service.cfg.ForceMigration = true
require.NoError(t, service.Run(ctx))
checkAlertingType(t, ctx, service, migrationStore.Legacy)
checkMigrationStatus(t, ctx, service, 1, false)
checkAlertRulesCount(t, x, 1, 0)
})
}
func checkMigrationStatus(t *testing.T, ctx context.Context, service *migrationService, orgID int64, expected bool) {
migrated, err := service.migrationStore.IsMigrated(ctx, orgID)
require.NoError(t, err)
require.Equal(t, expected, migrated)
}
func checkAlertingType(t *testing.T, ctx context.Context, service *migrationService, expected migrationStore.AlertingType) {
aType, err := service.migrationStore.GetCurrentAlertingType(ctx)
require.NoError(t, err)
require.Equal(t, expected, aType)
}
func checkAlertRulesCount(t *testing.T, x *xorm.Engine, orgID int64, count int) {
cnt, err := x.Table("alert_rule").Where("org_id=?", orgID).Count()
require.NoError(t, err, "table alert_rule error")
require.Equal(t, int(cnt), count, "table alert_rule should have no rows")
}
type testcase struct {
name string
orgToMigrate int64
skipExisting bool
// Common Inputs
folders []*dashboards.Dashboard
dashboards []*dashboards.Dashboard
dashboardPerms map[string][]accesscontrol.SetResourcePermissionCommand
initialLegacyState legacyState
initialUAState *uaState
operations []testOp
}
type legacyState struct {
alerts []*legacymodels.Alert
channels []*legacymodels.AlertNotification
}
type uaState struct {
alerts []*models.AlertRule
amConfig *definitions.PostableUserConfig
migState *migrationStore.OrgMigrationState
serviceState *definitions.OrgMigrationState // output only.
}
type testOp struct {
description string
newLegacyState *legacyState
updateLegacyState *legacyState
operation func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error
expectedUAState *uaState
expectedErrors []string
}
//nolint:gocyclo
func TestCommonServicePatterns(t *testing.T) {
sh := newServiceHelper(t)
f1 := sh.genFolder()
f2 := sh.genFolder()
generalAlertingFolder := sh.genFolder()
generalAlertingFolder.UID = "general-alerting"
generalAlertingFolder.Title = "General Alerting"
sh.folders[generalAlertingFolder.ID] = generalAlertingFolder
sh.foldersByUID[generalAlertingFolder.UID] = generalAlertingFolder
generalFolder := &dashboards.Dashboard{
ID: 0,
Title: "General",
}
sh.folders[generalFolder.ID] = generalFolder
sh.foldersByUID[generalFolder.UID] = generalFolder
d1 := sh.genDash(f1)
alerts1 := sh.genAlerts(d1, 10)
rules1, pairs1 := sh.genAlertPairs(f1, d1, alerts1)
d2 := sh.genDash(f1)
alerts2 := sh.genAlerts(d2, 10)
rules2, pairs2 := sh.genAlertPairs(f1, d2, alerts2)
d3 := sh.genDash(f2)
alerts3 := sh.genAlerts(d3, 10)
_, pairs3 := sh.genAlertPairs(f2, d3, alerts3)
channels1 := sh.genChannels(10)
channels2 := sh.genChannels(10)
modifiedAlerts := func(alerts []*legacymodels.Alert, muts ...func(alert *legacymodels.Alert)) []*legacymodels.Alert {
newAlerts := copyAlerts(alerts...)
for _, alert := range newAlerts {
for _, mut := range muts {
mut(alert)
}
}
return newAlerts
}
modifiedSuffix := "-modified"
withModifiedName := func(alert *legacymodels.Alert) {
alert.Name = alert.Name + modifiedSuffix
}
withName := func(name string) func(alert *legacymodels.Alert) {
return func(alert *legacymodels.Alert) {
alert.Name = name
}
}
withNotifiers := func(alert *legacymodels.Alert) {
alert.Settings.Set("notifications", []notificationKey{{ID: alert.ID}})
}
modifiedPairs := func(pairs []*migmodels.AlertPair, muts ...func(alert *migmodels.AlertPair)) []*migmodels.AlertPair {
newPairs := copyPairs(pairs...)
for _, pair := range newPairs {
for _, mut := range muts {
mut(pair)
}
}
return newPairs
}
withModifiedTitle := func(pair *migmodels.AlertPair) {
pair.Rule.Title = pair.Rule.Title + modifiedSuffix
}
withTitle := func(name string) func(pair *migmodels.AlertPair) {
return func(pair *migmodels.AlertPair) {
pair.Rule.Title = name
pair.LegacyRule.Name = name
}
}
withNotifierLabels := func(pair *migmodels.AlertPair) {
withNotifiers(pair.LegacyRule)
pair.Rule.Labels[contactLabel(fmt.Sprintf("notifiername%d", pair.LegacyRule.ID))] = "true"
}
modifiedRules := func(alerts []*models.AlertRule, muts ...func(alert *models.AlertRule)) []*models.AlertRule {
newAlerts := copyRules(alerts...)
for _, alert := range newAlerts {
for _, mut := range muts {
mut(alert)
}
}
return newAlerts
}
withFolder := func(f *dashboards.Dashboard) func(alert *models.AlertRule) {
return func(alert *models.AlertRule) {
alert.NamespaceUID = f.UID
}
}
modifiedChannels := func(channels []*legacymodels.AlertNotification, muts ...func(c *legacymodels.AlertNotification)) []*legacymodels.AlertNotification {
newChannels := copyChannels(channels...)
for _, c := range newChannels {
for _, mut := range muts {
mut(c)
}
}
return newChannels
}
withModifiedChannelName := func(c *legacymodels.AlertNotification) {
c.Name = c.Name + modifiedSuffix
}
withType := func(t string) func(c *legacymodels.AlertNotification) {
return func(c *legacymodels.AlertNotification) {
c.Type = t
}
}
modifiedState := func(state *uaState, muts ...func(state *uaState)) *uaState {
for _, mut := range muts {
mut(state)
}
return state
}
for _, tt := range []testcase{
{
name: "Standard org migration",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: alerts1,
channels: channels1,
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: sh.uaState(t, channels1, pairs1),
},
},
},
{
name: "Standard org migration with multiple dashboards",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1, d2, d3},
initialLegacyState: legacyState{
alerts: append(append(alerts1, alerts2...), alerts3...),
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: sh.uaState(t, nil, pairs1, pairs2, pairs3),
},
},
},
{
name: "with existing alerts & channels not from migration, doesn't delete existing",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: alerts1,
channels: channels1,
},
initialUAState: &uaState{ // Not connected to the migration.
alerts: rules2,
amConfig: createPostableUserConfig(t, channels2...),
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: modifiedState(sh.uaState(t, channels1, pairs1), func(state *uaState) {
state.alerts = append(state.alerts, rules2...)
state.amConfig = createPostableUserConfig(t, append(channels1, channels2...)...)
}),
},
{
description: "all dashboards migration, no change",
operation: migrateAllDashboardAlertsOp(false),
expectedUAState: modifiedState(sh.uaState(t, channels1, pairs1), func(state *uaState) {
state.alerts = append(state.alerts, rules2...)
state.amConfig = createPostableUserConfig(t, append(channels1, channels2...)...)
}),
},
{
description: "all channels migration, no change",
operation: migrateAllChannelsOp(false),
expectedUAState: modifiedState(sh.uaState(t, channels1, pairs1), func(state *uaState) {
state.alerts = append(state.alerts, rules2...)
state.amConfig = createPostableUserConfig(t, append(channels1, channels2...)...)
}),
},
{
description: "dashboard d1 migration, no change",
operation: migrateDashboardAlertsOp(false, d1.ID),
expectedUAState: modifiedState(sh.uaState(t, channels1, pairs1), func(state *uaState) {
state.alerts = append(state.alerts, rules2...)
state.amConfig = createPostableUserConfig(t, append(channels1, channels2...)...)
}),
},
{
description: "channels[0] migration, no change",
operation: migrateChannelOp(d1.ID, channels1[0].ID),
expectedUAState: modifiedState(sh.uaState(t, channels1, pairs1), func(state *uaState) {
state.alerts = append(state.alerts, rules2...)
state.amConfig = createPostableUserConfig(t, append(channels1, channels2...)...)
}),
},
{
description: "alerts d1[0] migration, no change",
operation: migrateAlertOp(d1.ID, alerts1[0].PanelID),
expectedUAState: modifiedState(sh.uaState(t, channels1, pairs1), func(state *uaState) {
state.alerts = append(state.alerts, rules2...)
state.amConfig = createPostableUserConfig(t, append(channels1, channels2...)...)
}),
},
},
},
{
name: "migrateAlert with existing alerts in different folder",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: alerts1[:5:5],
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: sh.uaState(t, nil, pairs1[:5:5]),
},
{
description: "move dashboard d1 to folder f2",
operation: func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
d1Copy := *d1
//nolint:staticcheck
d1Copy.FolderID = f2.ID
_, err := x.ID(d1.ID).Update(d1Copy)
return err
},
},
{
description: "add more alerts to d1 and migrate them",
newLegacyState: &legacyState{
alerts: alerts1[5:6:6],
},
operation: migrateAlertOp(d1.ID, alerts1[5].PanelID),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1[:6:6]), func(state *uaState) {
state.serviceState.MigratedDashboards[0].FolderUID = f2.UID
state.serviceState.MigratedDashboards[0].FolderName = f2.Title
}),
},
{
description: "add more alerts to d1 and migrate them using migrate dashboard skipExisting=true",
newLegacyState: &legacyState{
alerts: alerts1[6:10:10],
},
operation: migrateDashboardAlertsOp(true, d1.ID),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1), func(state *uaState) {
state.serviceState.MigratedDashboards[0].FolderUID = f2.UID
state.serviceState.MigratedDashboards[0].FolderName = f2.Title
}),
},
{
description: "migrate with skipExisting=false should move all the alerts to f2",
operation: migrateDashboardAlertsOp(false, d1.ID),
expectedUAState: modifiedState(sh.uaState(t, nil, modifiedPairs(pairs1, func(p *migmodels.AlertPair) { p.Rule.NamespaceUID = f2.UID })), func(state *uaState) {
state.serviceState.MigratedDashboards[0].FolderUID = f2.UID
state.serviceState.MigratedDashboards[0].FolderName = f2.Title
}),
},
},
},
{
name: "migrate alerts using various skipExisting",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1, d2, d3},
initialLegacyState: legacyState{
alerts: alerts1,
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: sh.uaState(t, nil, pairs1),
},
{
description: "add some alerts for d2 & d3 and migrate dashboard d2",
newLegacyState: &legacyState{
alerts: append(alerts2[:5:5], alerts3[:5:5]...),
},
operation: migrateDashboardAlertsOp(false, d2.ID),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1, pairs2[:5:5]), func(state *uaState) {
state.serviceState.MigratedDashboards = append(state.serviceState.MigratedDashboards, &definitions.DashboardUpgrade{
DashboardID: d3.ID,
DashboardUID: d3.UID,
DashboardName: d3.Title,
FolderUID: f2.UID,
FolderName: f2.Title,
MigratedAlerts: make([]*definitions.AlertPair, 5),
Error: "dashboard not upgraded",
})
for i, a := range alerts3[:5:5] {
state.serviceState.MigratedDashboards[2].MigratedAlerts[i] = &definitions.AlertPair{
LegacyAlert: fromLegacyAlert(a),
Error: "alert not upgraded",
}
}
}),
},
{
description: "modify the alerts already migrated for d2 and add the rest, migrate dashboard d2 with skipExisting",
updateLegacyState: &legacyState{
alerts: modifiedAlerts(alerts2[:5:5], withModifiedName),
},
newLegacyState: &legacyState{
alerts: alerts2[5:],
},
operation: migrateDashboardAlertsOp(true, d2.ID),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1, pairs2), func(state *uaState) {
for i := 0; i < 5; i++ {
state.serviceState.MigratedDashboards[1].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
}
state.serviceState.MigratedDashboards = append(state.serviceState.MigratedDashboards, &definitions.DashboardUpgrade{
DashboardID: d3.ID,
DashboardUID: d3.UID,
DashboardName: d3.Title,
FolderUID: f2.UID,
FolderName: f2.Title,
MigratedAlerts: make([]*definitions.AlertPair, 5),
Error: "dashboard not upgraded",
})
for i, a := range alerts3[:5:5] {
state.serviceState.MigratedDashboards[2].MigratedAlerts[i] = &definitions.AlertPair{
LegacyAlert: fromLegacyAlert(a),
Error: "alert not upgraded",
}
}
}), // Because of skipExisting, expected doesn't contain the modifications.
},
{
description: "migrate dashboard d3 using migrate all dashboards",
operation: migrateAllDashboardAlertsOp(true),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1, pairs2, pairs3[:5:5]), func(state *uaState) {
for i := 0; i < 5; i++ {
state.serviceState.MigratedDashboards[1].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
}
}),
},
{
description: "migrate dashboard d2 with skipExisting=false should update using the modifications",
operation: migrateDashboardAlertsOp(false, d2.ID),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1, append(modifiedPairs(pairs2[:5:5], withModifiedTitle), pairs2[5:]...), pairs3[:5:5]), func(state *uaState) {
for i := 0; i < 5; i++ {
state.serviceState.MigratedDashboards[1].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
}
}),
},
{
description: "modify existing d3 alerts and add the rest, migrate one new alert on dashboard d3, should not update modifications",
updateLegacyState: &legacyState{
alerts: modifiedAlerts(alerts3[:5:5], withModifiedName),
},
newLegacyState: &legacyState{
alerts: alerts3[5:],
},
operation: migrateAlertOp(d3.ID, alerts3[5].PanelID),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1, append(modifiedPairs(pairs2[:5:5], withModifiedTitle), pairs2[5:]...), pairs3[:6:6]), func(state *uaState) {
for i := 0; i < 5; i++ {
state.serviceState.MigratedDashboards[1].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
state.serviceState.MigratedDashboards[2].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
}
for _, a := range alerts3[6:] {
state.serviceState.MigratedDashboards[2].MigratedAlerts = append(state.serviceState.MigratedDashboards[2].MigratedAlerts, &definitions.AlertPair{
LegacyAlert: fromLegacyAlert(a),
Error: "alert not upgraded",
})
}
}),
},
{
description: "migrate one existing alert on dashboard d3, should update modifications",
operation: migrateAlertOp(d3.ID, alerts3[0].PanelID),
expectedUAState: modifiedState(sh.uaState(t, nil, pairs1, append(modifiedPairs(pairs2[:5:5], withModifiedTitle), pairs2[5:]...), append(modifiedPairs(pairs3[0:1:1], withModifiedTitle), pairs3[1:6:6]...)), func(state *uaState) {
for i := 0; i < 5; i++ {
state.serviceState.MigratedDashboards[1].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
state.serviceState.MigratedDashboards[2].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
}
for _, a := range alerts3[6:] {
state.serviceState.MigratedDashboards[2].MigratedAlerts = append(state.serviceState.MigratedDashboards[2].MigratedAlerts, &definitions.AlertPair{
LegacyAlert: fromLegacyAlert(a),
Error: "alert not upgraded",
})
}
}),
},
{
description: "update d1 alerts, and re-migrate all dashboards",
updateLegacyState: &legacyState{
alerts: modifiedAlerts(alerts1, withModifiedName),
},
operation: migrateAllDashboardAlertsOp(false),
expectedUAState: modifiedState(sh.uaState(t, nil, modifiedPairs(pairs1, withModifiedTitle), append(modifiedPairs(pairs2[:5:5], withModifiedTitle), pairs2[5:]...), append(modifiedPairs(pairs3[0:5:5], withModifiedTitle), pairs3[5:]...)), func(state *uaState) {
for i := 0; i < 5; i++ {
state.serviceState.MigratedDashboards[1].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
state.serviceState.MigratedDashboards[2].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
}
for i := 0; i < 10; i++ {
state.serviceState.MigratedDashboards[0].MigratedAlerts[i].LegacyAlert.Name += modifiedSuffix
}
}),
},
},
},
{
name: "MigrateAllChannels skip=true doesn't update existing channels",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
initialUAState: sh.uaState(t, channels1),
operations: []testOp{
{
updateLegacyState: &legacyState{channels: modifiedChannels(channels1, withModifiedChannelName)},
operation: migrateAllChannelsOp(true),
expectedUAState: modifiedState(sh.uaState(t, channels1), func(state *uaState) {
for _, c := range state.serviceState.MigratedChannels {
c.LegacyChannel.Name += modifiedSuffix
}
}),
},
},
},
{
name: "MigrateAllChannels skip=false updates existing channels",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
initialUAState: sh.uaState(t, channels1),
operations: []testOp{
{
updateLegacyState: &legacyState{channels: modifiedChannels(channels1, withModifiedChannelName)},
operation: migrateAllChannelsOp(false),
expectedUAState: sh.uaState(t, modifiedChannels(channels1, withModifiedChannelName)),
},
},
},
{
name: "MigrateAllChannels skip=false doesn't delete existing channels unrelated to migration",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
initialUAState: &uaState{
amConfig: createPostableUserConfig(t, channels2...),
},
operations: []testOp{
{
operation: migrateAllChannelsOp(false),
expectedUAState: modifiedState(sh.uaState(t, channels1), func(state *uaState) {
state.amConfig = createPostableUserConfig(t, append(channels1, channels2...)...)
}),
},
},
},
{
name: "MigrateAllChannels skip=true adds new channels",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
initialUAState: sh.uaState(t, channels1),
operations: []testOp{
{
newLegacyState: &legacyState{channels: channels2},
operation: migrateAllChannelsOp(true),
expectedUAState: sh.uaState(t, append(channels1, channels2...)),
},
},
},
{
name: "MigrateAllChannels skip=false adds new channels",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
initialUAState: sh.uaState(t, channels1),
operations: []testOp{
{
newLegacyState: &legacyState{channels: channels2},
operation: migrateAllChannelsOp(false),
expectedUAState: sh.uaState(t, append(channels1, channels2...)),
},
},
},
{
name: "MigrateSingleChannel adds new channel and doesn't affect others",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
initialUAState: sh.uaState(t, channels1),
operations: []testOp{
{
newLegacyState: &legacyState{channels: channels2[0:1:1]},
operation: migrateChannelOp(channels2[0].ID),
expectedUAState: sh.uaState(t, append(channels1, channels2[0])),
},
},
},
{
name: "MigrateSingleChannel updates existing channel and doesn't affect others",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
initialUAState: sh.uaState(t, channels1),
operations: []testOp{
{
updateLegacyState: &legacyState{channels: modifiedChannels(channels1, withModifiedChannelName)},
operation: migrateChannelOp(channels1[9].ID),
expectedUAState: modifiedState(sh.uaState(t, append(channels1[0:9:9], modifiedChannels(channels1[9:], withModifiedChannelName)...)), func(state *uaState) {
for i := 0; i < 10; i++ {
state.serviceState.MigratedChannels[i].LegacyChannel.Name = channels1[i].Name + modifiedSuffix
}
}),
},
},
},
{
name: "MigrateSingleChannel removes deleted channel and doesn't affect others",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1[0:9:9]},
initialUAState: sh.uaState(t, channels1), // Existing state has channels1[9].
operations: []testOp{
{
operation: migrateChannelOp(channels1[9].ID),
expectedUAState: sh.uaState(t, channels1[0:9:9]),
},
},
},
{
name: "MigrateSingleChannel doesn't delete existing channels unrelated to migration",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: []*legacymodels.AlertNotification{channels1[0]}},
initialUAState: &uaState{
amConfig: createPostableUserConfig(t, channels2...),
},
operations: []testOp{
{
operation: migrateChannelOp(channels1[0].ID),
expectedUAState: modifiedState(sh.uaState(t, channels1[0:1:1]), func(state *uaState) {
state.amConfig = createPostableUserConfig(t, append(channels2, channels1[0])...)
}),
},
},
},
{
name: "alert titles should be deduplicated",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: modifiedAlerts(alerts1, withName("duplicate name")),
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: modifiedState(sh.uaState(t, nil, modifiedPairs(pairs1, withTitle("duplicate name"))), func(state *uaState) {
state.alerts[0].Title = "duplicate name"
for i := 1; i < len(state.alerts); i++ { // First pair doesn't need to be deduplicated.
state.alerts[i].Title = fmt.Sprintf("duplicate name #%d", i+1)
}
for i := 0; i < len(state.alerts); i++ {
state.serviceState.MigratedDashboards[0].MigratedAlerts[i].AlertRule.Title = state.alerts[i].Title
}
}),
},
},
},
{
name: "alert titles should be truncated",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: modifiedAlerts(alerts1[0:1:1], withName(strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1))),
},
operations: []testOp{
{
operation: migrateOrgOp,
expectedUAState: modifiedState(sh.uaState(t, nil, modifiedPairs(pairs1[0:1:1], withTitle(strings.Repeat("a", store.AlertDefinitionMaxTitleLength)))), func(state *uaState) {
state.serviceState.MigratedDashboards[0].MigratedAlerts[0].LegacyAlert.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
})},
},
},
{
name: "alert titles should be truncated and deduplicated",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: modifiedAlerts(alerts1, withName(strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1))),
},
operations: []testOp{
{
operation: migrateOrgOp,
expectedUAState: func() *uaState {
pairs := modifiedPairs(pairs1, withTitle(strings.Repeat("a", store.AlertDefinitionMaxTitleLength)))
for i := 1; i < len(pairs); i++ { // First pair doesn't need to be deduplicated.
suffix := fmt.Sprintf(" #%d", i+1)
pairs[i].Rule.Title = fmt.Sprintf("%s%s", pairs[i].Rule.Title[:store.AlertDefinitionMaxTitleLength-len(suffix)], suffix)
}
state := sh.uaState(t, nil, pairs)
for i := 0; i < len(pairs); i++ {
state.serviceState.MigratedDashboards[0].MigratedAlerts[i].LegacyAlert.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
}
return state
}(),
},
},
},
{
name: "alert has invalid settings, should return pair with error",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
// Break the last half of the alerts.
alerts: append(alerts1[:5:5], modifiedAlerts(alerts1[5:10:10], func(alert *legacymodels.Alert) { alert.Settings.Set("noDataState", 1.5) })...),
},
operations: []testOp{
{
operation: migrateOrgOp,
expectedUAState: &uaState{
alerts: rules1[:5:5],
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
MigratedDashboards: map[int64]*migrationStore.DashboardUpgrade{
d1.ID: sh.dashUpgrade(d1.ID, f1.UID, append(pairs1[:5:5], modifiedPairs(pairs1[5:10:10], func(pair *migmodels.AlertPair) {
pair.Error = errors.New("parse settings: json: cannot unmarshal number into Go struct field dashAlertSettings.noDataState of type string")
pair.Rule.UID = ""
})...), ""),
},
},
},
},
},
},
{
name: "alert has missing dashboard, should return pair with error",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
// Set dashboard to nonexisting id for the last half of the alerts.
alerts: append(alerts1[:5:5], modifiedAlerts(alerts1[5:10:10], func(alert *legacymodels.Alert) { alert.DashboardID = -42 })...),
},
operations: []testOp{
{
operation: migrateOrgOp,
expectedUAState: &uaState{
alerts: rules1[:5:5],
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
MigratedDashboards: map[int64]*migrationStore.DashboardUpgrade{
d1.ID: sh.dashUpgrade(d1.ID, f1.UID, pairs1[:5:5], ""),
-42: sh.dashUpgrade(-42, "", modifiedPairs(pairs1[5:10:10], func(pair *migmodels.AlertPair) {
pair.Error = errors.New("orphaned: missing dashboard")
pair.Rule.UID = ""
}), ""),
},
},
},
},
},
},
{
name: "alert dashboard has missing folder, should migrate to new general alerting folder",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, generalAlertingFolder},
//nolint:staticcheck
dashboards: []*dashboards.Dashboard{func(d dashboards.Dashboard) *dashboards.Dashboard { d.FolderID = 99999; return &d }(*d1), d2},
initialLegacyState: legacyState{
alerts: append(alerts1, alerts2...),
},
operations: []testOp{
{
operation: migrateOrgOp,
expectedUAState: &uaState{
alerts: append(modifiedRules(rules1, withFolder(generalAlertingFolder)), rules2...),
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
MigratedDashboards: map[int64]*migrationStore.DashboardUpgrade{
d1.ID: sh.dashUpgrade(d1.ID, generalAlertingFolder.UID, pairs1, "dashboard alerts moved to general alerting folder during upgrade: original folder not found"),
d2.ID: sh.dashUpgrade(d2.ID, f1.UID, pairs2, ""),
},
},
},
},
},
},
{
name: "alert dashboard in general folder, should migrate to new general alerting folder",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, generalAlertingFolder},
//nolint:staticcheck
dashboards: []*dashboards.Dashboard{func(d dashboards.Dashboard) *dashboards.Dashboard { d.FolderID = 0; return &d }(*d1), d2},
initialLegacyState: legacyState{
alerts: append(alerts1[0:1:1], alerts2[0]),
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: &uaState{
alerts: append(modifiedRules(rules1[0:1:1], withFolder(generalAlertingFolder)), rules2[0]),
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
MigratedDashboards: map[int64]*migrationStore.DashboardUpgrade{
d1.ID: sh.dashUpgrade(d1.ID, generalAlertingFolder.UID, pairs1[0:1:1], "dashboard alerts moved to general alerting folder during upgrade: general folder not supported"),
d2.ID: sh.dashUpgrade(d2.ID, f1.UID, pairs2[0:1:1], ""),
},
},
serviceState: &definitions.OrgMigrationState{
OrgID: 1,
MigratedDashboards: []*definitions.DashboardUpgrade{
{
DashboardID: d1.ID,
DashboardUID: d1.UID,
DashboardName: d1.Title,
FolderUID: generalFolder.UID,
FolderName: generalFolder.Title,
NewFolderUID: generalAlertingFolder.UID,
NewFolderName: generalAlertingFolder.Title,
MigratedAlerts: []*definitions.AlertPair{
{LegacyAlert: fromLegacyAlert(alerts1[0]), AlertRule: fromAlertRuleUpgrade(rules1[0], []string{"autogen-contact-point-default"})},
},
Warning: "dashboard alerts moved to general alerting folder during upgrade: general folder not supported",
},
{
DashboardID: d2.ID,
DashboardUID: d2.UID,
DashboardName: d2.Title,
FolderUID: f1.UID,
FolderName: f1.Title,
NewFolderUID: f1.UID,
NewFolderName: f1.Title,
MigratedAlerts: []*definitions.AlertPair{
{LegacyAlert: fromLegacyAlert(alerts2[0]), AlertRule: fromAlertRuleUpgrade(rules2[0], []string{"autogen-contact-point-default"})},
},
},
},
},
},
},
},
},
{
name: "alert dashboard has custom permissions, should migrate to new folder",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{
f1,
func() *dashboards.Dashboard {
// The folder name is deterministic, so we can create the expected folder beforehand. This is so we know the uid for expected states.
f := createFolder(t, 100, 1, "created-folder-id")
f.Title = "folder1 Alerts - 787427ef800a01a544d6bae21970b4d2"
return f
}(),
},
dashboards: []*dashboards.Dashboard{d1, d2},
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
d2.UID: {{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_ADMIN.String()}}, // This permission maps to the 787427ef800a01a544d6bae21970b4d2 hash above.
},
initialLegacyState: legacyState{
alerts: append(alerts1, alerts2...),
},
operations: []testOp{
{
operation: migrateOrgOp,
expectedUAState: &uaState{
alerts: append(rules1, modifiedRules(rules2, withFolder(&dashboards.Dashboard{UID: "created-folder-id"}))...),
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
MigratedDashboards: map[int64]*migrationStore.DashboardUpgrade{
d1.ID: sh.dashUpgrade(d1.ID, f1.UID, pairs1, ""),
d2.ID: sh.dashUpgrade(d2.ID, "created-folder-id", pairs2, "dashboard alerts moved to new folder during upgrade: folder permission changes were needed"),
},
},
},
},
},
},
{
name: "channel is discontinued, should return pair with error",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
operations: []testOp{
{
updateLegacyState: &legacyState{channels: modifiedChannels([]*legacymodels.AlertNotification{channels1[8], channels1[9]}, withType("hipchat"))},
operation: migrateAllChannelsOp(false),
expectedUAState: &uaState{
amConfig: createPostableUserConfig(t, channels1[:8:8]...),
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
MigratedChannels: func() map[int64]*migrationStore.ContactPair {
pairs := sh.contactPairs(channels1...)
pairs[channels1[8].ID].Error = "'hipchat': discontinued"
pairs[channels1[8].ID].NewReceiverUID = ""
pairs[channels1[9].ID].Error = "'hipchat': discontinued"
pairs[channels1[9].ID].NewReceiverUID = ""
return pairs
}(),
},
},
},
},
},
{
name: "channel name updates are reflected in alert labels",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: modifiedAlerts(alerts1, withNotifiers),
channels: channels1,
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: sh.uaState(t, channels1, modifiedPairs(pairs1, withNotifierLabels)),
},
{
description: "update channel names and migrate single channel",
updateLegacyState: &legacyState{
channels: modifiedChannels(channels1, withModifiedChannelName),
},
operation: migrateChannelOp(channels1[9].ID),
expectedUAState: modifiedState(sh.uaState(t,
append(channels1[:9:9], modifiedChannels(channels1[9:10:10], withModifiedChannelName)...),
append(modifiedPairs(pairs1[:9:9], withNotifierLabels), modifiedPairs(pairs1[9:10:10], func(a *migmodels.AlertPair) {
withNotifiers(a.LegacyRule)
a.Rule.Labels[contactLabel(fmt.Sprintf("notifiername%d-modified", a.LegacyRule.ID))] = "true"
})...),
), func(state *uaState) {
for i := range pairs1 {
// Service state knows the updated legacy channel names.
state.serviceState.MigratedChannels[i].LegacyChannel.Name = channels1[i].Name + modifiedSuffix
}
}),
},
{
description: "migrate the rest of the channels",
operation: migrateAllChannelsOp(false),
expectedUAState: sh.uaState(t,
modifiedChannels(channels1, withModifiedChannelName),
modifiedPairs(pairs1, func(a *migmodels.AlertPair) {
withNotifiers(a.LegacyRule)
a.Rule.Labels[contactLabel(fmt.Sprintf("notifiername%d-modified", a.LegacyRule.ID))] = "true"
}),
),
},
},
},
{
name: "contact point name updates are reflected in alert labels",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: modifiedAlerts(alerts1, withNotifiers),
channels: channels1,
},
initialUAState: modifiedState(sh.uaState(t, channels1, modifiedPairs(pairs1, withNotifierLabels)), func(state *uaState) {
// Update all the contact points names. Done here so we simulate it being done post-migration.
for i := 0; i < 10; i++ {
state.amConfig.AlertmanagerConfig.Receivers[i+1].Name += modifiedSuffix
state.amConfig.AlertmanagerConfig.Receivers[i+1].GrafanaManagedReceivers[0].Name += modifiedSuffix
state.amConfig.AlertmanagerConfig.Route.Routes[0].Routes[i].Receiver += modifiedSuffix
}
}),
operations: []testOp{
{
description: "re-migrated alerts should get the label to route to the correct contact point",
operation: migrateAllDashboardAlertsOp(false),
expectedUAState: modifiedState(sh.uaState(t, channels1, modifiedPairs(pairs1, withNotifierLabels)), func(state *uaState) {
for i := range pairs1 {
state.serviceState.MigratedDashboards[0].MigratedAlerts[i].AlertRule.SendsTo = []string{channels1[i].Name + modifiedSuffix}
state.serviceState.MigratedChannels[i].ContactPointUpgrade.Name += modifiedSuffix
}
for i := 0; i < 10; i++ {
state.amConfig.AlertmanagerConfig.Receivers[i+1].Name += modifiedSuffix
state.amConfig.AlertmanagerConfig.Receivers[i+1].GrafanaManagedReceivers[0].Name += modifiedSuffix
state.amConfig.AlertmanagerConfig.Route.Routes[0].Routes[i].Receiver += modifiedSuffix
}
}),
},
},
},
{
name: "alert labels are correct when when alert is migrated before channel",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: modifiedAlerts(alerts1[:5:5], withNotifiers),
channels: channels1[:5:5],
},
operations: []testOp{
{
description: "initial migration of first 5 alerts and channels",
operation: migrateOrgOp,
expectedUAState: sh.uaState(t, channels1[:5:5], modifiedPairs(pairs1[:5:5], withNotifierLabels)),
},
{
description: "add the last 5 alerts and channels, migrate just the last 5 alerts",
newLegacyState: &legacyState{
alerts: modifiedAlerts(alerts1[5:10:10], withNotifiers),
channels: channels1[5:10:10],
},
operation: migrateAllDashboardAlertsOp(true),
},
{
description: "migrate the last 5 channels",
operation: migrateAllChannelsOp(true),
expectedUAState: sh.uaState(t, channels1, modifiedPairs(pairs1, withNotifierLabels)),
},
},
},
{
name: "empty folders previously created by migration should be deleted",
orgToMigrate: 1,
folders: []*dashboards.Dashboard{f1, f2},
dashboards: []*dashboards.Dashboard{d1},
initialLegacyState: legacyState{
alerts: alerts1,
},
initialUAState: &uaState{
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
CreatedFolders: []string{f1.UID},
},
},
operations: []testOp{
{
description: "initial migration",
operation: migrateOrgOp,
expectedUAState: func() *uaState {
state := sh.uaState(t, nil, pairs1)
state.migState.CreatedFolders = []string{f1.UID}
return state
}(),
},
{
description: "move dashboard d1 to folder f2",
operation: func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
d1Copy := *d1
//nolint:staticcheck
d1Copy.FolderID = f2.ID
_, err := x.ID(d1.ID).Update(d1Copy)
return err
},
},
{
description: "migrate with skipExisting=false should move all the alerts to f2 and cleanup f1",
operation: migrateDashboardAlertsOp(false, d1.ID),
expectedUAState: modifiedState(sh.uaState(t, nil, modifiedPairs(pairs1, func(p *migmodels.AlertPair) { p.Rule.NamespaceUID = f2.UID })), func(state *uaState) {
state.serviceState.MigratedDashboards[0].FolderUID = f2.UID
state.serviceState.MigratedDashboards[0].FolderName = f2.Title
}),
},
},
},
{
name: "unmigrated channels should show up in GetOrgMigration state",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1},
operations: []testOp{
{
operation: migrateChannelOp(channels1[0].ID),
expectedUAState: modifiedState(sh.uaState(t, channels1[0:1:1]), func(state *uaState) {
for _, c := range channels1[1:] {
state.serviceState.MigratedChannels = append(state.serviceState.MigratedChannels, &definitions.ContactPair{
LegacyChannel: fromLegacyChannel(c),
Error: "channel not upgraded",
})
}
}),
},
},
},
{
name: "channels deleted after migration should show up in GetOrgMigration state",
orgToMigrate: 1,
initialLegacyState: legacyState{channels: channels1[0:9:9]},
initialUAState: sh.uaState(t, channels1), // Existing state has channels1[9].
operations: []testOp{
{
expectedUAState: modifiedState(sh.uaState(t, channels1), func(state *uaState) {
for _, c := range state.serviceState.MigratedChannels {
if c.LegacyChannel.ID == channels1[9].ID {
c.LegacyChannel = &definitions.LegacyChannel{ID: c.LegacyChannel.ID}
c.Error = "channel no longer exists"
}
}
}),
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
tcRun(t, tt)
})
}
}
var migrateOrgOp = func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
_, err := service.MigrateOrg(ctx, tt.orgToMigrate, tt.skipExisting)
if err != nil {
return err
}
return nil
}
var migrateAllDashboardAlertsOp = func(skipExisting bool) func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
return func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
_, err := service.MigrateAllDashboardAlerts(ctx, tt.orgToMigrate, skipExisting)
if err != nil {
return err
}
return nil
}
}
var migrateAllChannelsOp = func(skipExisting bool) func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
return func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
_, err := service.MigrateAllChannels(ctx, tt.orgToMigrate, skipExisting)
if err != nil {
return err
}
return nil
}
}
var migrateDashboardAlertsOp = func(skipExisting bool, ids ...int64) func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
return func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
for _, id := range ids {
_, err := service.MigrateDashboardAlerts(ctx, tt.orgToMigrate, id, skipExisting)
if err != nil {
return err
}
}
return nil
}
}
var migrateChannelOp = func(ids ...int64) func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
return func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
for _, id := range ids {
_, err := service.MigrateChannel(ctx, tt.orgToMigrate, id)
if err != nil {
return err
}
}
return nil
}
}
var migrateAlertOp = func(dashboardId int64, panelIds ...int64) func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
return func(ctx context.Context, tt testcase, service *migrationService, x *xorm.Engine) error {
for _, id := range panelIds {
_, err := service.MigrateAlert(ctx, tt.orgToMigrate, dashboardId, id)
if err != nil {
return err
}
}
return nil
}
}
func tcRun(t *testing.T, tt testcase) {
sqlStore := db.InitTestDB(t)
x := sqlStore.GetEngine()
store := &store.DBstore{
SQLStore: sqlStore,
Logger: &logtest.Fake{},
Cfg: setting.UnifiedAlertingSettings{
BaseInterval: 10 * time.Second,
DefaultRuleEvaluationInterval: time.Minute,
},
}
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
defer teardown(t, x, service)
setupLegacyAlertsTables(t, x, tt.initialLegacyState.channels, tt.initialLegacyState.alerts, tt.folders, tt.dashboards)
if tt.initialUAState == nil {
tt.initialUAState = &uaState{}
}
setupUATables(t, store, tt.orgToMigrate, tt.initialUAState.alerts, tt.initialUAState.amConfig)
if tt.initialUAState.migState != nil {
require.NoError(t, service.migrationStore.SetOrgMigrationState(context.Background(), tt.orgToMigrate, tt.initialUAState.migState))
}
if tt.dashboardPerms != nil {
for uid, perms := range tt.dashboardPerms {
_, err := service.migrationStore.SetDashboardPermissions(context.Background(), 1, uid, perms...)
require.NoError(t, err)
}
}
ctx := context.Background()
require.NoError(t, service.migrationStore.SetMigrated(context.Background(), tt.orgToMigrate, true)) // To bypass verification.
for _, op := range tt.operations {
if op.description != "" {
t.Logf("Running operation: %s", op.description)
}
if op.newLegacyState != nil {
err := sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
if len(op.newLegacyState.channels) > 0 {
_, err := sess.Insert(op.newLegacyState.channels)
require.NoError(t, err)
}
if len(op.newLegacyState.alerts) > 0 {
_, err := sess.Insert(op.newLegacyState.alerts)
require.NoError(t, err)
}
return nil
})
require.NoError(t, err)
}
if op.updateLegacyState != nil {
err := sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
for _, c := range op.updateLegacyState.channels {
_, err := sess.ID(c.ID).Update(c)
require.NoError(t, err)
}
for _, a := range op.updateLegacyState.alerts {
_, err := sess.ID(a.ID).Update(a)
require.NoError(t, err)
}
return nil
})
require.NoError(t, err)
}
if op.operation != nil {
err := op.operation(ctx, tt, service, x)
if len(op.expectedErrors) > 0 {
for _, expErr := range op.expectedErrors {
require.ErrorContains(t, err, expErr)
}
return
}
require.NoError(t, err)
}
if op.expectedUAState != nil {
compareRules(t, x, tt.orgToMigrate, op.expectedUAState.alerts)
compareAmConfig(t, x, tt.orgToMigrate, op.expectedUAState.amConfig)
compareState(t, x, service, tt.orgToMigrate, op.expectedUAState.migState, op.expectedUAState.serviceState)
}
}
}
func compareRules(t *testing.T, x *xorm.Engine, orgId int64, expectedRules []*models.AlertRule) {
if expectedRules == nil {
return
}
rules := make([]*models.AlertRule, 0)
err := x.Table("alert_rule").Where("org_id = ?", orgId).Find(&rules)
require.NoError(t, err)
cOpt := []cmp.Option{
cmpopts.SortSlices(func(a, b models.AlertRule) bool {
return a.Title < b.Title
}),
cmpopts.IgnoreUnexported(models.AlertRule{}, models.AlertQuery{}),
cmpopts.IgnoreFields(models.AlertRule{}, "Updated", "UID", "ID", "Version"),
cmpopts.IgnoreMapEntries(func(k string, v string) bool { return k == "rule_uid" }),
}
if !cmp.Equal(expectedRules, rules, cOpt...) {
t.Errorf("Unexpected Rule: %v", cmp.Diff(expectedRules, rules, cOpt...))
}
}
func compareAmConfig(t *testing.T, x *xorm.Engine, orgId int64, expectedConfig *definitions.PostableUserConfig) {
if expectedConfig == nil {
return
}
amConfig := getAlertmanagerConfig(t, x, orgId)
// Order of nested GrafanaManagedReceivers is not guaranteed.
cOpt := []cmp.Option{
cmpopts.IgnoreUnexported(definitions.PostableApiReceiver{}),
cmpopts.IgnoreFields(definitions.PostableGrafanaReceiver{}, "UID", "SecureSettings"),
cmpopts.SortSlices(func(a, b *definitions.PostableGrafanaReceiver) bool { return a.Name < b.Name }),
cmpopts.SortSlices(func(a, b *definitions.PostableApiReceiver) bool { return a.Name < b.Name }),
}
if !cmp.Equal(expectedConfig.AlertmanagerConfig.Receivers, amConfig.AlertmanagerConfig.Receivers, cOpt...) {
t.Errorf("Unexpected Receivers: %v", cmp.Diff(expectedConfig.AlertmanagerConfig.Receivers, amConfig.AlertmanagerConfig.Receivers, cOpt...))
}
// Order of routes is not guaranteed.
cOpt = []cmp.Option{
cmpopts.SortSlices(func(a, b *definitions.Route) bool {
if a.Receiver != b.Receiver {
return a.Receiver < b.Receiver
}
return a.ObjectMatchers[0].Value < b.ObjectMatchers[0].Value
}),
cmpopts.IgnoreUnexported(definitions.Route{}, labels.Matcher{}),
cmpopts.IgnoreFields(definitions.Route{}, "GroupBy", "GroupByAll"),
}
if !cmp.Equal(expectedConfig.AlertmanagerConfig.Route, amConfig.AlertmanagerConfig.Route, cOpt...) {
t.Errorf("Unexpected Route: %v", cmp.Diff(expectedConfig.AlertmanagerConfig.Route, amConfig.AlertmanagerConfig.Route, cOpt...))
}
}
func compareState(t *testing.T, x *xorm.Engine, service *migrationService, orgId int64, expectedState *migrationStore.OrgMigrationState, expectedServiceState *definitions.OrgMigrationState) {
if expectedState == nil && expectedServiceState == nil {
return
}
// Assign real UIDS to expected state for comparison.
type ruleUid struct {
DashboardID int64 `xorm:"dashboard_id"`
PanelID int64 `xorm:"panel_id"`
UID string `xorm:"uid"`
}
ruleUids := make([]ruleUid, 0)
err := x.SQL("SELECT d.id as dashboard_id, ar.panel_id, ar.uid FROM alert_rule ar INNER JOIN dashboard d ON d.uid = ar.dashboard_uid WHERE ar.org_id = ?", orgId).Find(&ruleUids)
require.NoError(t, err)
uidMap := make(map[string]string)
for _, r := range ruleUids {
if du, ok := expectedState.MigratedDashboards[r.DashboardID]; ok {
if _, ok := du.MigratedAlerts[r.PanelID]; ok {
uidMap[du.MigratedAlerts[r.PanelID].NewRuleUID] = r.UID
du.MigratedAlerts[r.PanelID].NewRuleUID = r.UID
}
}
}
state, err := service.migrationStore.GetOrgMigrationState(context.Background(), orgId)
require.NoError(t, err)
cOpt := []cmp.Option{
cmpopts.SortSlices(func(a, b string) bool { return a < b }),
cmpopts.EquateEmpty(),
}
if !cmp.Equal(expectedState, state, cOpt...) {
t.Errorf("Unexpected OrgMigrationState: %v", cmp.Diff(expectedState, state, cOpt...))
}
if expectedServiceState != nil {
for _, du := range expectedServiceState.MigratedDashboards {
for _, a := range du.MigratedAlerts {
if a.AlertRule != nil {
a.AlertRule.UID = uidMap[a.AlertRule.UID]
}
}
}
serviceState, err := service.GetOrgMigrationState(context.Background(), orgId)
require.NoError(t, err)
cOpt := []cmp.Option{
cmpopts.SortSlices(func(a, b *definitions.DashboardUpgrade) bool { return a.DashboardID < b.DashboardID }),
cmpopts.SortSlices(func(a, b *definitions.AlertPair) bool { return a.LegacyAlert.ID < b.LegacyAlert.ID }),
cmpopts.SortSlices(func(a, b *definitions.ContactPair) bool { return a.LegacyChannel.ID < b.LegacyChannel.ID }),
cmpopts.IgnoreMapEntries(func(k string, v string) bool { return k == "rule_uid" }),
cmpopts.IgnoreUnexported(labels.Matcher{}),
cmpopts.EquateEmpty(),
}
if !cmp.Equal(expectedServiceState, serviceState, cOpt...) {
t.Errorf("Unexpected OrgMigrationState: %v", cmp.Diff(expectedServiceState, serviceState, cOpt...))
}
}
}
// setupUATables inserts data into the UA tables.
func setupUATables(t *testing.T, store *store.DBstore, orgID int64, rules []*models.AlertRule, amConfig *definitions.PostableUserConfig) {
t.Helper()
ctx := context.Background()
rs := make([]models.AlertRule, 0, len(rules))
for _, r := range rules {
rs = append(rs, *r)
}
if len(rs) > 0 {
_, err := store.InsertAlertRules(ctx, rs)
require.NoError(t, err)
}
if amConfig != nil {
rawAmConfig, err := json.Marshal(amConfig)
require.NoError(t, err)
cmd := models.SaveAlertmanagerConfigurationCmd{
AlertmanagerConfiguration: string(rawAmConfig),
ConfigurationVersion: fmt.Sprintf("v%d", models.AlertConfigurationVersion),
Default: false,
OrgID: orgID,
LastApplied: 0,
}
err = store.SaveAlertmanagerConfiguration(ctx, &cmd)
require.NoError(t, err)
}
}
func createPostableUserConfig(t *testing.T, channels ...*legacymodels.AlertNotification) *definitions.PostableUserConfig {
t.Helper()
am := &definitions.PostableUserConfig{
AlertmanagerConfig: definitions.PostableApiAlertingConfig{
Config: definitions.Config{Route: &definitions.Route{
Receiver: "autogen-contact-point-default",
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel},
Routes: []*definitions.Route{
{
ObjectMatchers: definitions.ObjectMatchers{{Type: labels.MatchEqual, Name: models.MigratedUseLegacyChannelsLabel, Value: "true"}},
Continue: true,
Routes: []*definitions.Route{},
},
},
}},
Receivers: []*definitions.PostableApiReceiver{
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{}},
},
},
}
for _, c := range channels {
settings, err := c.Settings.MarshalJSON()
require.NoError(t, err)
am.AlertmanagerConfig.Receivers = append(am.AlertmanagerConfig.Receivers, &definitions.PostableApiReceiver{Receiver: config.Receiver{Name: c.Name}, PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{{UID: c.UID, Name: c.Name, Type: c.Type, Settings: settings}}}})
am.AlertmanagerConfig.Route.Routes[0].Routes = append(am.AlertmanagerConfig.Route.Routes[0].Routes, &definitions.Route{Receiver: c.Name, ObjectMatchers: definitions.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel(c.Name), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)})
}
return am
}
type serviceHelper struct {
t *testing.T
dashIncr int64
alertIncr int64
ruleIncr int64
channelIncr int64
dashes map[int64]*dashboards.Dashboard
folders map[int64]*dashboards.Dashboard
foldersByUID map[string]*dashboards.Dashboard
}
func newServiceHelper(t *testing.T) serviceHelper {
return serviceHelper{
t: t,
dashIncr: int64(1),
alertIncr: int64(1),
ruleIncr: int64(1),
channelIncr: int64(1),
dashes: make(map[int64]*dashboards.Dashboard),
folders: make(map[int64]*dashboards.Dashboard),
foldersByUID: make(map[string]*dashboards.Dashboard),
}
}
func (h *serviceHelper) genAlerts(d *dashboards.Dashboard, cnt int) []*legacymodels.Alert {
d.Title = fmt.Sprintf("dash title%d", h.dashIncr)
alerts := make([]*legacymodels.Alert, 0, cnt)
for i := 0; i < cnt; i++ {
a := createAlertWithCond(h.t, 1, int(d.ID), int(h.alertIncr), fmt.Sprintf("alert%d", h.alertIncr), nil,
[]dashAlertCondition{createCondition("A", "max", "gt", 42, 1, "5m", "now")})
a.ID = h.alertIncr
alerts = append(alerts, a)
h.alertIncr++
}
h.dashIncr++
return alerts
}
func (h *serviceHelper) genFolder() *dashboards.Dashboard {
f := createFolder(h.t, h.dashIncr, 1, fmt.Sprintf("folder%d", h.dashIncr))
h.dashIncr++
h.folders[f.ID] = f
h.foldersByUID[f.UID] = f
return f
}
func (h *serviceHelper) genDash(folder *dashboards.Dashboard) *dashboards.Dashboard {
d := createDashboard(h.t, h.dashIncr, 1, fmt.Sprintf("dash%d", h.dashIncr), folder.ID, nil)
d.Title = fmt.Sprintf("dash title%d", h.dashIncr)
h.dashIncr++
h.dashes[d.ID] = d
return d
}
func (h *serviceHelper) genChannels(cnt int) []*legacymodels.AlertNotification {
channels := make([]*legacymodels.AlertNotification, 0, cnt)
for i := 0; i < cnt; i++ {
c := createAlertNotification(h.t, int64(1), fmt.Sprintf("notifier%d", h.channelIncr), "email", emailSettings, false)
c.Name = fmt.Sprintf("notifiername%d", h.channelIncr)
c.ID = h.channelIncr
channels = append(channels, c)
h.channelIncr++
}
return channels
}
func (h *serviceHelper) genAlertPairs(f *dashboards.Dashboard, d *dashboards.Dashboard, alerts []*legacymodels.Alert) ([]*models.AlertRule, []*migmodels.AlertPair) {
pairs := make([]*migmodels.AlertPair, 0, len(alerts))
rules := make([]*models.AlertRule, 0, len(alerts))
for _, a := range alerts {
uid := util.GenerateShortUID()
r := &models.AlertRule{
UID: uid,
ID: h.ruleIncr,
OrgID: 1,
Title: a.Name,
Condition: "B",
Data: []models.AlertQuery{createAlertQuery("A", "ds1-1", "5m", "now"), createClassicConditionQuery("B", []classicCondition{
cond("A", "max", "gt", 42),
})},
IntervalSeconds: 60,
Version: 1,
NamespaceUID: f.UID,
DashboardUID: pointer(d.UID),
PanelID: pointer(a.PanelID),
RuleGroup: fmt.Sprintf("%s - 1m", d.Title),
RuleGroupIndex: 1,
NoDataState: models.NoData,
ExecErrState: models.AlertingErrState,
For: 60 * time.Second,
Annotations: map[string]string{
models.MigratedAlertIdAnnotation: fmt.Sprintf("%d", a.ID),
models.MigratedMessageAnnotation: "message",
models.DashboardUIDAnnotation: d.UID,
models.PanelIDAnnotation: fmt.Sprintf("%d", a.PanelID),
},
Labels: map[string]string{
models.MigratedUseLegacyChannelsLabel: "true",
"rule_uid": uid,
},
IsPaused: false,
}
for _, v := range extractChannelIds(h.t, a) {
id := v.ID
if id != 0 {
// Relies on the naming pattern.
r.Labels[contactLabel(fmt.Sprintf("notifiername%d", id))] = "true"
}
}
rules = append(rules, r)
pairs = append(pairs, &migmodels.AlertPair{
LegacyRule: a,
Rule: r,
})
h.ruleIncr++
}
return rules, pairs
}
func (h *serviceHelper) dashUpgrade(dashboardID int64, alertFolderUID string, migPairs []*migmodels.AlertPair, warning string) *migrationStore.DashboardUpgrade {
return &migrationStore.DashboardUpgrade{
DashboardID: dashboardID,
AlertFolderUID: alertFolderUID,
MigratedAlerts: func() map[int64]*migrationStore.AlertPair {
pairs := make(map[int64]*migrationStore.AlertPair, len(migPairs))
for _, p := range migPairs {
channelsIds := make([]int64, 0)
for _, v := range extractChannelIds(h.t, p.LegacyRule) {
channelsIds = append(channelsIds, v.ID)
}
pair := migrationStore.AlertPair{
LegacyID: p.LegacyRule.ID,
PanelID: p.LegacyRule.PanelID,
NewRuleUID: p.Rule.UID,
ChannelIDs: channelsIds,
}
if p.Error != nil {
pair.Error = p.Error.Error()
}
pairs[p.LegacyRule.PanelID] = &pair
}
return pairs
}(),
Warning: warning,
}
}
func (h *serviceHelper) contactPairs(c ...*legacymodels.AlertNotification) map[int64]*migrationStore.ContactPair {
pairs := make(map[int64]*migrationStore.ContactPair, len(c))
for _, ch := range c {
pairs[ch.ID] = &migrationStore.ContactPair{
LegacyID: ch.ID,
NewReceiverUID: ch.UID,
Error: "",
}
}
return pairs
}
func (h *serviceHelper) uaState(t *testing.T, channels []*legacymodels.AlertNotification, dashPairs ...[]*migmodels.AlertPair) *uaState {
s := &uaState{
migState: &migrationStore.OrgMigrationState{
OrgID: 1,
},
serviceState: h.serviceState(channels, dashPairs...),
}
if len(channels) > 0 {
s.amConfig = createPostableUserConfig(t, channels...)
s.migState.MigratedChannels = h.contactPairs(channels...)
}
if len(dashPairs) > 0 {
s.migState.MigratedDashboards = map[int64]*migrationStore.DashboardUpgrade{}
for _, pairs := range dashPairs {
for _, p := range pairs {
s.alerts = append(s.alerts, p.Rule)
}
s.migState.MigratedDashboards[pairs[0].LegacyRule.DashboardID] = h.dashUpgrade(pairs[0].LegacyRule.DashboardID, pairs[0].Rule.NamespaceUID, pairs, "")
}
}
return s
}
func (h *serviceHelper) serviceState(channels []*legacymodels.AlertNotification, dashPairs ...[]*migmodels.AlertPair) *definitions.OrgMigrationState {
state := &definitions.OrgMigrationState{
OrgID: 1,
MigratedDashboards: []*definitions.DashboardUpgrade{},
MigratedChannels: []*definitions.ContactPair{},
}
channelName := make(map[int64]string)
for _, c := range channels {
channelName[c.ID] = c.Name
}
for _, pairs := range dashPairs {
d := h.dashes[pairs[0].LegacyRule.DashboardID]
//nolint:staticcheck
f := h.folders[d.FolderID]
f2 := h.foldersByUID[pairs[0].Rule.NamespaceUID]
du := &definitions.DashboardUpgrade{
DashboardID: d.ID,
DashboardUID: d.UID,
DashboardName: d.Title,
FolderUID: f.UID,
FolderName: f.Title,
NewFolderUID: f2.UID,
NewFolderName: f2.Title,
Provisioned: false,
Warning: "",
}
for _, pair := range pairs {
var sendsTo []string
for _, v := range extractChannelIds(h.t, pair.LegacyRule) {
sendsTo = append(sendsTo, channelName[v.ID])
}
if len(sendsTo) == 0 {
sendsTo = []string{"autogen-contact-point-default"}
}
p := &definitions.AlertPair{
LegacyAlert: fromLegacyAlert(pair.LegacyRule),
AlertRule: fromAlertRuleUpgrade(pair.Rule, sendsTo),
}
if pair.Error != nil {
p.Error = pair.Error.Error()
}
du.MigratedAlerts = append(du.MigratedAlerts, p)
}
state.MigratedDashboards = append(state.MigratedDashboards, du)
}
if len(channels) > 0 {
for _, c := range channels {
route, _ := createRoute(c, c.Name)
state.MigratedChannels = append(state.MigratedChannels, &definitions.ContactPair{
LegacyChannel: fromLegacyChannel(c),
ContactPointUpgrade: &definitions.ContactPointUpgrade{
Name: c.Name,
Type: c.Type,
RouteMatchers: route.ObjectMatchers,
},
})
}
}
return state
}
func copyMap(m map[string]string) map[string]string {
c := make(map[string]string, len(m))
for k, v := range m {
c[k] = v
}
return c
}
func copyAlerts(alerts ...*legacymodels.Alert) []*legacymodels.Alert {
copies := make([]*legacymodels.Alert, len(alerts))
for i, a := range alerts {
c := *a
settingsMap := c.Settings.MustMap()
c.Settings = simplejson.New()
for k, v := range settingsMap {
c.Settings.Set(k, v)
}
copies[i] = &c
}
return copies
}
func copyRules(rules ...*models.AlertRule) []*models.AlertRule {
copies := make([]*models.AlertRule, len(rules))
for i, a := range rules {
c := *a
c.Labels = copyMap(c.Labels)
c.Annotations = copyMap(c.Annotations)
copies[i] = &c
}
return copies
}
func copyChannels(channels ...*legacymodels.AlertNotification) []*legacymodels.AlertNotification {
copies := make([]*legacymodels.AlertNotification, len(channels))
for i, a := range channels {
c := *a
copies[i] = &c
}
return copies
}
func copyPairs(pairs ...*migmodels.AlertPair) []*migmodels.AlertPair {
newPairs := make([]*migmodels.AlertPair, len(pairs))
for i, pair := range pairs {
clr := copyAlerts(pair.LegacyRule)[0]
cr := copyRules(pair.Rule)[0]
newPairs[i] = &migmodels.AlertPair{
LegacyRule: clr,
Rule: cr,
Error: pair.Error,
}
}
return newPairs
}
func extractChannelIds(t *testing.T, alert *legacymodels.Alert) []notificationKey {
b, err := alert.Settings.Get("notifications").ToDB()
if err == nil && b != nil {
require.NoError(t, err)
var nots []notificationKey
err = json.Unmarshal(b, &nots)
require.NoError(t, err)
return nots
}
return nil
}
func fromLegacyAlert(alert *legacymodels.Alert) *definitions.LegacyAlert {
if alert == nil {
return nil
}
return &definitions.LegacyAlert{
ID: alert.ID,
DashboardID: alert.DashboardID,
PanelID: alert.PanelID,
Name: alert.Name,
}
}
func fromAlertRuleUpgrade(rule *models.AlertRule, sendsTo []string) *definitions.AlertRuleUpgrade {
if rule == nil {
return nil
}
return &definitions.AlertRuleUpgrade{
UID: rule.UID,
Title: rule.Title,
SendsTo: sendsTo,
}
}
func fromLegacyChannel(channel *legacymodels.AlertNotification) *definitions.LegacyChannel {
if channel == nil {
return nil
}
return &definitions.LegacyChannel{
ID: channel.ID,
Name: channel.Name,
Type: channel.Type,
}
}