2021-04-29 13:24:37 -04:00
package ualert
import (
"fmt"
"os"
2021-09-28 10:27:23 -04:00
"path/filepath"
"strconv"
2021-04-29 13:24:37 -04:00
2022-04-20 22:02:23 -04:00
"xorm.io/xorm"
2021-11-04 18:47:21 +02:00
2021-10-07 17:30:06 -04:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2021-10-22 10:11:06 +01:00
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
2021-04-29 13:24:37 -04:00
)
2021-05-11 08:08:39 -04:00
var migTitle = "move dashboard alerts to unified alerting"
2022-06-22 10:52:46 -04:00
const codeMigration = "code migration"
2021-08-12 16:04:09 +03:00
2021-04-29 13:24:37 -04:00
type MigrationError struct {
AlertId int64
Err error
}
func ( e MigrationError ) Error ( ) string {
return fmt . Sprintf ( "failed to migrate alert %d: %s" , e . AlertId , e . Err . Error ( ) )
}
func ( e * MigrationError ) Unwrap ( ) error { return e . Err }
2023-10-12 13:43:10 +01:00
// FixEarlyMigration fixes UA configs created before 8.2 with org_id=0 and moves some files like __default__.tmpl.
// The only use of this migration is when a user enabled ng-alerting before 8.2.
func FixEarlyMigration ( mg * migrator . Migrator ) {
2021-08-12 16:04:09 +03:00
cloneMigTitle := fmt . Sprintf ( "clone %s" , migTitle )
2023-10-12 13:43:10 +01:00
mg . AddMigration ( cloneMigTitle , & upgradeNgAlerting { } )
2021-08-12 16:04:09 +03:00
}
2021-10-04 16:33:55 +01:00
func AddDashboardUIDPanelIDMigration ( mg * migrator . Migrator ) {
migrationID := "update dashboard_uid and panel_id from existing annotations"
2023-10-12 13:43:10 +01:00
mg . AddMigration ( migrationID , & updateDashboardUIDPanelIDMigration { } )
2021-10-04 16:33:55 +01:00
}
// updateDashboardUIDPanelIDMigration sets the dashboard_uid and panel_id columns
// from the __dashboardUid__ and __panelId__ annotations.
type updateDashboardUIDPanelIDMigration struct {
migrator . MigrationBase
}
func ( m * updateDashboardUIDPanelIDMigration ) SQL ( _ migrator . Dialect ) string {
return "set dashboard_uid and panel_id migration"
}
func ( m * updateDashboardUIDPanelIDMigration ) Exec ( sess * xorm . Session , mg * migrator . Migrator ) error {
var results [ ] struct {
ID int64 ` xorm:"id" `
Annotations map [ string ] string ` xorm:"annotations" `
}
if err := sess . SQL ( ` SELECT id, annotations FROM alert_rule ` ) . Find ( & results ) ; err != nil {
return fmt . Errorf ( "failed to get annotations for all alert rules: %w" , err )
}
for _ , next := range results {
var (
dashboardUID * string
panelID * int64
)
2021-10-07 17:30:06 -04:00
if s , ok := next . Annotations [ ngmodels . DashboardUIDAnnotation ] ; ok {
2021-10-04 16:33:55 +01:00
dashboardUID = & s
}
2021-10-07 17:30:06 -04:00
if s , ok := next . Annotations [ ngmodels . PanelIDAnnotation ] ; ok {
2021-10-04 16:33:55 +01:00
i , err := strconv . ParseInt ( s , 10 , 64 )
if err != nil {
2021-10-07 17:30:06 -04:00
return fmt . Errorf ( "the %s annotation does not contain a valid Panel ID: %w" , ngmodels . PanelIDAnnotation , err )
2021-10-04 16:33:55 +01:00
}
panelID = & i
}
2021-10-06 11:34:11 +01:00
// We do not want to set panel_id to a non-nil value when dashboard_uid is nil
// as panel_id is not unique and so cannot be queried without its dashboard_uid.
// This can happen where users have deleted the dashboard_uid annotation but kept
// the panel_id annotation.
if dashboardUID != nil {
if _ , err := sess . Exec ( ` UPDATE alert_rule SET dashboard_uid = ?, panel_id = ? WHERE id = ? ` ,
dashboardUID ,
panelID ,
next . ID ) ; err != nil {
return fmt . Errorf ( "failed to set dashboard_uid and panel_id for alert rule: %w" , err )
}
2021-10-04 16:33:55 +01:00
}
}
return nil
}
2021-05-20 00:40:12 +05:30
type AlertConfiguration struct {
2021-08-12 16:04:09 +03:00
ID int64 ` xorm:"pk autoincr 'id'" `
OrgID int64 ` xorm:"org_id" `
2021-05-20 00:40:12 +05:30
AlertmanagerConfiguration string
ConfigurationVersion string
2021-06-04 15:52:41 +03:00
CreatedAt int64 ` xorm:"created" `
2021-04-29 13:24:37 -04:00
}
2021-05-11 08:08:39 -04:00
2021-09-28 10:27:23 -04:00
type upgradeNgAlerting struct {
migrator . MigrationBase
}
var _ migrator . CodeMigration = & upgradeNgAlerting { }
func ( u * upgradeNgAlerting ) Exec ( sess * xorm . Session , migrator * migrator . Migrator ) error {
firstOrgId , err := u . updateAlertConfigurations ( sess , migrator )
if err != nil {
return err
}
u . updateAlertmanagerFiles ( firstOrgId , migrator )
return nil
}
func ( u * upgradeNgAlerting ) updateAlertConfigurations ( sess * xorm . Session , migrator * migrator . Migrator ) ( int64 , error ) {
// if there are records with org_id == 0 then the feature flag was enabled before 8.2 that introduced org separation.
// if feature is enabled in 8.2 the migration "AddDashAlertMigration", which is effectively different from what was run in 8.1.x and earlier versions,
// will handle organizations correctly, and, therefore, nothing needs to be fixed
count , err := sess . Table ( & AlertConfiguration { } ) . Where ( "org_id = 0" ) . Count ( )
if err != nil {
return 0 , fmt . Errorf ( "failed to query table alert_configuration: %w" , err )
}
if count == 0 {
return 0 , nil // NOTHING TO DO
}
orgs := make ( [ ] int64 , 0 )
// get all org IDs sorted in ascending order
if err = sess . Table ( "org" ) . OrderBy ( "id" ) . Cols ( "id" ) . Find ( & orgs ) ; err != nil {
return 0 , fmt . Errorf ( "failed to query table org: %w" , err )
}
if len ( orgs ) == 0 { // should not really happen
migrator . Logger . Info ( "No organizations are found. Nothing to migrate" )
return 0 , nil
}
firstOrg := orgs [ 0 ]
// assigning all configurations to the first org because 0 does not usually point to any
migrator . Logger . Info ( "Assigning all existing records from alert_configuration to the first organization" , "org" , firstOrg )
_ , err = sess . Cols ( "org_id" ) . Where ( "org_id = 0" ) . Update ( & AlertConfiguration { OrgID : firstOrg } )
if err != nil {
return 0 , fmt . Errorf ( "failed to update org_id for all rows in the table alert_configuration: %w" , err )
}
// if there is a single organization it is safe to assume that all configurations belong to it.
if len ( orgs ) == 1 {
return firstOrg , nil
}
// if there are many organizations we cannot safely assume what organization an alert_configuration belongs to.
// Therefore, we apply the default configuration to all organizations. The previous version could be restored if needed.
migrator . Logger . Warn ( "Detected many organizations. The current alertmanager configuration will be replaced by the default one" )
configs := make ( [ ] * AlertConfiguration , 0 , len ( orgs ) )
for _ , org := range orgs {
configs = append ( configs , & AlertConfiguration {
AlertmanagerConfiguration : migrator . Cfg . UnifiedAlerting . DefaultConfiguration ,
// Since we are migration for a snapshot of the code, it is always going to migrate to
// the v1 config.
ConfigurationVersion : "v1" ,
OrgID : org ,
} )
}
_ , err = sess . InsertMulti ( configs )
if err != nil {
return 0 , fmt . Errorf ( "failed to add default alertmanager configurations to every organization: %w" , err )
}
return 0 , nil
}
// updateAlertmanagerFiles scans the existing alerting directory '<data_dir>/alerting' for known files.
// If argument 'orgId' is not 0 updateAlertmanagerFiles moves all known files to the directory <data_dir>/alerting/<orgId>.
// Otherwise, it deletes those files.
// pre-8.2 version put all configuration files into the root of alerting directory. Since 8.2 configuration files are put in organization specific directory
func ( u * upgradeNgAlerting ) updateAlertmanagerFiles ( orgId int64 , migrator * migrator . Migrator ) {
2023-08-30 08:46:47 -07:00
knownFiles := map [ string ] any { "__default__.tmpl" : nil , "silences" : nil , "notifications" : nil }
2021-09-28 10:27:23 -04:00
alertingDir := filepath . Join ( migrator . Cfg . DataPath , "alerting" )
// do not fail if something goes wrong because these files are not used anymore. the worst that can happen is that we leave some leftovers behind
deleteFile := func ( fileName string ) {
path := filepath . Join ( alertingDir , fileName )
migrator . Logger . Info ( "Deleting alerting configuration file" , "file" , fileName )
err := os . Remove ( path )
if err != nil {
migrator . Logger . Warn ( "Failed to delete file" , "file" , path , "error" , err )
}
}
moveFile := func ( fileName string ) {
alertingOrgDir := filepath . Join ( alertingDir , strconv . FormatInt ( orgId , 10 ) )
if err := os . MkdirAll ( alertingOrgDir , 0750 ) ; err != nil {
migrator . Logger . Error ( "Failed to create alerting directory for organization. Skip moving the file and delete it instead" , "target_dir" , alertingOrgDir , "org_id" , orgId , "error" , err , "file" , fileName )
deleteFile ( fileName )
return
}
err := os . Rename ( filepath . Join ( alertingDir , fileName ) , filepath . Join ( alertingOrgDir , fileName ) )
if err != nil {
migrator . Logger . Error ( "Failed to move alertmanager configuration file to organization." , "source_dir" , alertingDir , "target_dir" , alertingOrgDir , "org_id" , orgId , "error" , err , "file" , fileName )
deleteFile ( fileName )
}
}
entries , err := os . ReadDir ( alertingDir )
if err != nil {
if ! os . IsNotExist ( err ) {
keys := make ( [ ] string , 0 , len ( knownFiles ) )
for key := range knownFiles {
keys = append ( keys , key )
}
migrator . Logger . Warn ( "Failed to clean up alerting directory. There may be files that are not used anymore." , "path" , alertingDir , "files_to_delete" , keys , "error" , err )
}
}
for _ , entry := range entries {
_ , known := knownFiles [ entry . Name ( ) ]
if known {
if orgId == 0 {
deleteFile ( entry . Name ( ) )
} else {
moveFile ( entry . Name ( ) )
}
}
}
}
func ( u * upgradeNgAlerting ) SQL ( migrator . Dialect ) string {
2022-06-22 10:52:46 -04:00
return codeMigration
2021-09-28 10:27:23 -04:00
}