diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index 18a1f67f919..8197429c493 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -49,6 +49,7 @@ Scopes must have an order to ensure consistency and ease of search, this helps u - `grafana_alerting_ticker_last_consumed_tick_timestamp_seconds` - `grafana_alerting_ticker_next_tick_timestamp_seconds` - `grafana_alerting_ticker_interval_seconds` +- [ENHANCEMENT] Create folder 'General Alerting' when Grafana starts from the scratch #48866 - [FEATURE] Indicate whether routes are provisioned when GETting Alertmanager configuration #47857 - [FEATURE] Indicate whether contact point is provisioned when GETting Alertmanager configuration #48323 - [FEATURE] Indicate whether alert rule is provisioned when GETting the rule #48458 diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 760c0172604..254910d21f8 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -94,6 +94,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { addEntityEventsTableMigration(mg) addPublicDashboardMigration(mg) + ualert.CreateDefaultFoldersForAlertingMigration(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/ualert/permissions.go b/pkg/services/sqlstore/migrations/ualert/permissions.go index 6bcf87c0486..0765dcb0c68 100644 --- a/pkg/services/sqlstore/migrations/ualert/permissions.go +++ b/pkg/services/sqlstore/migrations/ualert/permissions.go @@ -4,7 +4,10 @@ import ( "fmt" "time" + "xorm.io/xorm" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/infra/metrics" @@ -40,9 +43,14 @@ type dashboardAcl struct { Updated time.Time } +type folderHelper struct { + sess *xorm.Session + mg *migrator.Migrator +} + // getOrCreateGeneralFolder returns the general folder under the specific organisation // If the general folder does not exist it creates it. -func (m *migration) getOrCreateGeneralFolder(orgID int64) (*dashboard, error) { +func (m *folderHelper) getOrCreateGeneralFolder(orgID int64) (*dashboard, error) { // there is a unique constraint on org_id, folder_id, title // there are no nested folders so the parent folder id is always 0 dashboard := dashboard{OrgId: orgID, FolderId: 0, Title: GENERAL_FOLDER} @@ -51,18 +59,17 @@ func (m *migration) getOrCreateGeneralFolder(orgID int64) (*dashboard, error) { return nil, err } else if !has { // create folder - result, err := m.createFolder(orgID, GENERAL_FOLDER) - if err != nil { - return nil, err - } - - return result, nil + return m.createGeneralFolder(orgID) } return &dashboard, nil } +func (m *folderHelper) createGeneralFolder(orgID int64) (*dashboard, error) { + return m.createFolder(orgID, GENERAL_FOLDER) +} + // returns the folder of the given dashboard (if exists) -func (m *migration) getFolder(dash dashboard, da dashAlert) (dashboard, error) { +func (m *folderHelper) getFolder(dash dashboard, da dashAlert) (dashboard, error) { // get folder if exists folder := dashboard{} if dash.FolderId > 0 { @@ -82,7 +89,7 @@ func (m *migration) getFolder(dash dashboard, da dashAlert) (dashboard, error) { // based on sqlstore.saveDashboard() // it should be called from inside a transaction -func (m *migration) createFolder(orgID int64, title string) (*dashboard, error) { +func (m *folderHelper) createFolder(orgID int64, title string) (*dashboard, error) { cmd := saveFolderCommand{ OrgId: orgID, FolderId: 0, @@ -129,7 +136,7 @@ func (m *migration) createFolder(orgID int64, title string) (*dashboard, error) return dash, nil } -func (m *migration) generateNewDashboardUid(orgId int64) (string, error) { +func (m *folderHelper) generateNewDashboardUid(orgId int64) (string, error) { for i := 0; i < 3; i++ { uid := util.GenerateShortUID() @@ -148,7 +155,7 @@ func (m *migration) generateNewDashboardUid(orgId int64) (string, error) { // based on SQLStore.UpdateDashboardACL() // it should be called from inside a transaction -func (m *migration) setACL(orgID int64, dashboardID int64, items []*dashboardAcl) error { +func (m *folderHelper) setACL(orgID int64, dashboardID int64, items []*dashboardAcl) error { if dashboardID <= 0 { return fmt.Errorf("folder id must be greater than zero for a folder permission") } @@ -247,7 +254,7 @@ func (m *migration) setACL(orgID int64, dashboardID int64, items []*dashboardAcl } // based on SQLStore.GetDashboardAclInfoList() -func (m *migration) getACL(orgID, dashboardID int64) ([]*dashboardAcl, error) { +func (m *folderHelper) getACL(orgID, dashboardID int64) ([]*dashboardAcl, error) { var err error falseStr := m.mg.Dialect.BooleanStr(false) @@ -279,3 +286,18 @@ func (m *migration) getACL(orgID, dashboardID int64) ([]*dashboardAcl, error) { err = m.sess.SQL(rawSQL, orgID, dashboardID).Find(&result) return result, err } + +// getOrgsThatHaveFolders returns a unique list of organization ID that have at least one folder +func (m *folderHelper) getOrgsIDThatHaveFolders() (map[int64]struct{}, error) { + // get folder if exists + var rows []int64 + err := m.sess.Table(&dashboard{}).Where("is_folder=?", true).Distinct("org_id").Find(&rows) + if err != nil { + return nil, err + } + result := make(map[int64]struct{}, len(rows)) + for _, s := range rows { + result[s] = struct{}{} + } + return result, nil +} diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index 3c66ddb33d1..8ecadd7bfc0 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -223,7 +223,7 @@ func (m *migration) SQL(dialect migrator.Dialect) string { return "code migration" } -//nolint: gocyclo +// nolint: gocyclo func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { m.sess = sess m.mg = mg @@ -276,6 +276,11 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { } } + folderHelper := folderHelper{ + sess: sess, + mg: mg, + } + var folder *dashboard switch { case dash.HasAcl: @@ -284,21 +289,21 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { 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) // create folder and assign the permissions of the dashboard (included default and inherited) - f, err = m.createFolder(dash.OrgId, folderName) + f, err = folderHelper.createFolder(dash.OrgId, folderName) if err != nil { return MigrationError{ Err: fmt.Errorf("failed to create folder: %w", err), AlertId: da.Id, } } - permissions, err := m.getACL(dash.OrgId, dash.Id) + permissions, err := folderHelper.getACL(dash.OrgId, dash.Id) if err != nil { return MigrationError{ Err: fmt.Errorf("failed to get dashboard %d under organisation %d permissions: %w", dash.Id, dash.OrgId, err), AlertId: da.Id, } } - err = m.setACL(f.OrgId, f.Id, permissions) + err = folderHelper.setACL(f.OrgId, f.Id, permissions) if err != nil { return MigrationError{ Err: fmt.Errorf("failed to set folder %d under organisation %d permissions: %w", folder.Id, folder.OrgId, err), @@ -310,7 +315,7 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { folder = f case dash.FolderId > 0: // get folder if exists - f, err := m.getFolder(dash, da) + f, err := folderHelper.getFolder(dash, da) if err != nil { return MigrationError{ Err: err, @@ -322,7 +327,7 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { f, ok := folderCache[GENERAL_FOLDER] if !ok { // get or create general folder - f, err = m.getOrCreateGeneralFolder(dash.OrgId) + f, err = folderHelper.getOrCreateGeneralFolder(dash.OrgId) if err != nil { return MigrationError{ Err: fmt.Errorf("failed to get or create general folder under organisation %d: %w", dash.OrgId, err), @@ -753,3 +758,58 @@ func getAlertFolderNameFromDashboard(dash *dashboard) string { } return fmt.Sprintf(DASHBOARD_FOLDER, title, dash.Uid) // include UID to the name to avoid collision } + +// CreateDefaultFoldersForAlertingMigration creates a folder dedicated for alerting if no folders exist +func CreateDefaultFoldersForAlertingMigration(mg *migrator.Migrator) { + if !mg.Cfg.UnifiedAlerting.IsEnabled() { + return + } + mg.AddMigration("create default alerting folders", &createDefaultFoldersForAlertingMigration{}) +} + +type createDefaultFoldersForAlertingMigration struct { + migrator.MigrationBase +} + +func (c createDefaultFoldersForAlertingMigration) Exec(sess *xorm.Session, migrator *migrator.Migrator) error { + helper := folderHelper{ + sess: sess, + mg: migrator, + } + + var rows []struct { + Id int64 + Name string + } + + if err := sess.Table("org").Cols("id", "name").Find(&rows); err != nil { + return fmt.Errorf("failed to read the list of organizations: %w", err) + } + + orgsWithFolders, err := helper.getOrgsIDThatHaveFolders() + if err != nil { + return fmt.Errorf("failed to list organizations that have at least one folder: %w", err) + } + + for _, row := range rows { + // if there's at least one folder in the org or if alerting is disabled for that org, skip adding the default folder + if _, ok := orgsWithFolders[row.Id]; ok { + migrator.Logger.Debug("Skip adding default alerting folder because organization already has at least one folder", "org_id", row.Id) + continue + } + if _, ok := migrator.Cfg.UnifiedAlerting.DisabledOrgs[row.Id]; ok { + migrator.Logger.Debug("Skip adding default alerting folder because alerting is disabled for the organization ", "org_id", row.Id) + continue + } + folder, err := helper.createGeneralFolder(row.Id) + if err != nil { + return fmt.Errorf("failed to create the default alerting folder for organization %s (ID: %d): %w", row.Name, row.Id, err) + } + migrator.Logger.Info("created the default folder for alerting", "org_id", row.Id, "folder_name", folder.Title, "folder_uid", folder.Uid) + } + return nil +} + +func (c createDefaultFoldersForAlertingMigration) SQL(migrator.Dialect) string { + return "code migration" +}