diff --git a/pkg/services/sqlstore/migrations/ualert/migration_test.go b/pkg/services/sqlstore/migrations/ualert/migration_test.go index 2b441c510b2..c8536c91d83 100644 --- a/pkg/services/sqlstore/migrations/ualert/migration_test.go +++ b/pkg/services/sqlstore/migrations/ualert/migration_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" + ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -459,6 +460,49 @@ func TestDashAlertMigration(t *testing.T) { } } +// TestDashAlertMigration tests the execution of the main DashAlertMigration specifically for migrations of models. +func TestDashAlertMigrationFolders(t *testing.T) { + // Run initial migration to have a working DB. + x := setupTestDB(t) + + t.Run("when folder is missing put alert in General folder", func(t *testing.T) { + o := createOrg(t, 1) + folder1 := createDashboard(t, 1, o.Id, "folder-1") + folder1.IsFolder = true + dash1 := createDashboard(t, 3, o.Id, "dash1") + dash1.FolderId = folder1.Id + dash2 := createDashboard(t, 4, o.Id, "dash2") + dash2.FolderId = 22 // missing folder + + a1 := createAlert(t, o.Id, dash1.Id, int64(1), "alert-1", []string{}) + a2 := createAlert(t, o.Id, dash2.Id, int64(1), "alert-2", []string{}) + + _, err := x.Insert(o, folder1, dash1, dash2, a1, a2) + require.NoError(t, err) + + runDashAlertMigrationTestRun(t, x) + + rules := getAlertRules(t, x, o.Id) + require.Len(t, rules, 2) + + var generalFolder models.Dashboard + _, err = x.Table(&models.Dashboard{}).Where("title = ? AND org_id = ?", ualert.GENERAL_FOLDER, o.Id).Get(&generalFolder) + require.NoError(t, err) + + require.NotNil(t, generalFolder) + + for _, rule := range rules { + var expectedFolder models.Dashboard + if rule.Title == a1.Name { + expectedFolder = *folder1 + } else { + expectedFolder = generalFolder + } + require.Equal(t, expectedFolder.Uid, rule.NamespaceUID) + } + }) +} + // setupTestDB prepares the sqlite database and runs OSS migrations to initialize the schemas. func setupTestDB(t *testing.T) *xorm.Engine { t.Helper() @@ -593,6 +637,18 @@ func teardown(t *testing.T, x *xorm.Engine) { require.NoError(t, err) } +func runDashAlertMigrationTestRun(t *testing.T, x *xorm.Engine) { + _, errDeleteMig := x.Exec("DELETE FROM migration_log WHERE migration_id = ?", ualert.MigTitle) + require.NoError(t, errDeleteMig) + + alertMigrator := migrator.NewMigrator(x, &setting.Cfg{}) + alertMigrator.AddMigration(ualert.RmMigTitle, &ualert.RmMigration{}) + ualert.AddDashAlertMigration(alertMigrator) + + errRunningMig := alertMigrator.Start(false, 0) + require.NoError(t, errRunningMig) +} + // setupLegacyAlertsTables inserts data into the legacy alerting tables that is needed for testing the migration. func setupLegacyAlertsTables(t *testing.T, x *xorm.Engine, legacyChannels []*models.AlertNotification, alerts []*models.Alert) { t.Helper() @@ -637,6 +693,14 @@ func setupLegacyAlertsTables(t *testing.T, x *xorm.Engine, legacyChannels []*mod } } +func getAlertRules(t *testing.T, x *xorm.Engine, orgId int64) []*ngModels.AlertRule { + rules := make([]*ngModels.AlertRule, 0) + err := x.Table("alert_rule").Where("org_id = ?", orgId).Find(&rules) + require.NoError(t, err) + + return rules +} + // getAlertmanagerConfig retreives the Alertmanager Config from the database for a given orgId. func getAlertmanagerConfig(t *testing.T, x *xorm.Engine, orgId int64) *ualert.PostableUserConfig { amConfig := "" diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index affd5aee479..53024bf96ff 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -248,11 +248,31 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { // cache for folders created for dashboards that have custom permissions folderCache := make(map[string]*dashboard) + + gf := func(dash dashboard, da dashAlert) (*dashboard, error) { + f, ok := folderCache[GENERAL_FOLDER] + if !ok { + // get or create general folder + f, err = m.getOrCreateGeneralFolder(dash.OrgId) + if err != nil { + return nil, MigrationError{ + Err: fmt.Errorf("failed to get or create general folder under organisation %d: %w", dash.OrgId, err), + AlertId: da.Id, + } + } + folderCache[GENERAL_FOLDER] = f + } + // No need to assign default permissions to general folder + // because they are included to the query result if it's a folder with no permissions + // https://github.com/grafana/grafana/blob/076e2ce06a6ecf15804423fcc8dca1b620a321e5/pkg/services/sqlstore/dashboard_acl.go#L109 + return f, nil + } // Store of newly created rules to later create routes rulesPerOrg := make(map[int64]map[string]dashAlert) for _, da := range dashAlerts { + l := mg.Logger.New("ruleID", da.Id, "ruleName", da.Name, "dashboardUID", da.DashboardUID, "orgID", da.OrgId) newCond, err := transConditions(*da.ParsedSettings, da.OrgId, dsIDMap) if err != nil { return err @@ -282,7 +302,7 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { folderName := getAlertFolderNameFromDashboard(&dash) f, ok := folderCache[folderName] if !ok { - mg.Logger.Info("create a new folder for alerts that belongs to dashboard because it has custom permissions", "org", dash.OrgId, "dashboard_uid", dash.Uid, "folder", folderName) + l.Info("create a new folder for alerts that belongs to dashboard because it has custom permissions", "folder", folderName) // create folder and assign the permissions of the dashboard (included default and inherited) f, err = m.createFolder(dash.OrgId, folderName) if err != nil { @@ -312,29 +332,20 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { // get folder if exists f, err := m.getFolder(dash, da) if err != nil { - return MigrationError{ - Err: err, - AlertId: da.Id, - } - } - folder = &f - default: - f, ok := folderCache[GENERAL_FOLDER] - if !ok { - // get or create general folder - f, err = m.getOrCreateGeneralFolder(dash.OrgId) + // If folder does not exist then the dashboard is an orphan and we migrate the alert to the general folder. + l.Warn("Failed to find folder for dashboard. Migrate rule to the default folder", "rule_name", da.Name, "dashboard_uid", da.DashboardUID, "missing_folder_id", dash.FolderId) + folder, err = gf(dash, da) if err != nil { - return MigrationError{ - Err: fmt.Errorf("failed to get or create general folder under organisation %d: %w", dash.OrgId, err), - AlertId: da.Id, - } + return err } - folderCache[GENERAL_FOLDER] = f + } else { + folder = &f + } + default: + folder, err = gf(dash, da) + if err != nil { + return err } - // No need to assign default permissions to general folder - // because they are included to the query result if it's a folder with no permissions - // https://github.com/grafana/grafana/blob/076e2ce06a6ecf15804423fcc8dca1b620a321e5/pkg/services/sqlstore/dashboard_acl.go#L109 - folder = f } if folder.Uid == "" {