grafana/pkg/services/ngalert/migration/service_test.go
Sofia Papagiannaki 89d3b55bec
Folders: Reduce DB queries when counting and deleting resources under folders (#81153)
* Add folder store method for fetching all folder descendants

* Modify GetDescendantCounts() to fetch folder descendants at once

* Reduce DB calls when counting library panels under dashboard

* Reduce DB calls when counting dashboards under folder

* Reduce DB calls during folder delete

* Modify folder registry to count/delete entities under multiple folders

* Reduce DB calls when counting

* Reduce DB calls when deleting
2024-01-30 18:26:34 +02:00

2133 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", "folder5-1", 5, nil),
createDashboard(t, 2, 1, "dash2-1", "folder5-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.UID, 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
d1Copy.FolderUID = f2.UID
_, 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"),
}
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.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.UID, 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",
},
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,
}
}