2023-10-12 13:43:10 +01:00
package migration
import (
"context"
2024-01-05 18:19:12 -05:00
"encoding/json"
"errors"
2023-11-30 10:25:59 -05:00
"fmt"
2024-01-05 18:19:12 -05:00
"strings"
2023-10-12 13:43:10 +01:00
"testing"
"time"
2024-01-05 18:19:12 -05:00
"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"
2023-10-12 13:43:10 +01:00
"github.com/stretchr/testify/require"
2024-01-05 18:19:12 -05:00
"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"
2023-11-30 10:25:59 -05:00
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
2024-01-05 18:19:12 -05:00
"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"
2023-11-30 10:25:59 -05:00
"xorm.io/xorm"
2023-10-12 13:43:10 +01:00
"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 {
2024-01-26 16:36:35 +01:00
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 ) ,
2023-10-12 13:43:10 +01:00
}
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 )
2023-11-30 10:25:59 -05:00
err = service . migrationStore . SetCurrentAlertingType ( ctx , migrationStore . Legacy )
2023-10-12 13:43:10 +01:00
require . NoError ( t , err )
2023-11-30 10:25:59 -05:00
require . NoError ( t , service . Run ( ctx ) )
2023-10-12 13:43:10 +01:00
// Verify migration was run.
2023-11-30 10:25:59 -05:00
checkAlertingType ( t , ctx , service , migrationStore . UnifiedAlerting )
checkMigrationStatus ( t , ctx , service , 1 , true )
2023-10-12 13:43:10 +01:00
// Currently, we fill in some random data for tables that aren't populated during migration.
2023-11-30 10:25:59 -05:00
_ , err = x . Table ( "ngalert_configuration" ) . Insert ( models . AdminConfiguration { OrgID : 1 } )
2023-10-12 13:43:10 +01:00
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
2023-11-30 10:25:59 -05:00
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" } ,
2023-10-12 13:43:10 +01:00
}
for _ , table := range tables {
2023-11-30 10:25:59 -05:00
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 ] )
2023-10-12 13:43:10 +01:00
}
// Revert migration.
2023-11-30 10:25:59 -05:00
err = service . migrationStore . RevertAllOrgs ( context . Background ( ) )
2023-10-12 13:43:10 +01:00
require . NoError ( t , err )
// Verify revert was run.
2023-11-30 10:25:59 -05:00
checkAlertingType ( t , ctx , service , migrationStore . Legacy )
checkMigrationStatus ( t , ctx , service , 1 , false )
2023-10-12 13:43:10 +01:00
// Verify various UA resources are gone
for _ , table := range tables {
2023-11-30 10:25:59 -05:00
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 ] )
2023-10-12 13:43:10 +01:00
}
} )
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 )
2023-11-30 10:25:59 -05:00
err = service . migrationStore . SetCurrentAlertingType ( ctx , migrationStore . Legacy )
2023-10-12 13:43:10 +01:00
require . NoError ( t , err )
2023-11-30 10:25:59 -05:00
require . NoError ( t , service . Run ( ctx ) )
2023-10-12 13:43:10 +01:00
// Verify migration was run.
2023-11-30 10:25:59 -05:00
checkAlertingType ( t , ctx , service , migrationStore . UnifiedAlerting )
checkMigrationStatus ( t , ctx , service , 1 , true )
2023-10-12 13:43:10 +01:00
// 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.
2023-11-30 10:25:59 -05:00
err = service . migrationStore . RevertAllOrgs ( context . Background ( ) )
2023-10-12 13:43:10 +01:00
require . NoError ( t , err )
2023-11-30 10:25:59 -05:00
// Verify revert was run. Should only set migration status for org.
checkAlertingType ( t , ctx , service , migrationStore . Legacy )
checkMigrationStatus ( t , ctx , service , 1 , false )
2023-10-12 13:43:10 +01:00
// 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 ) )
}
} )
2023-10-12 23:40:51 +01:00
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 ) ,
2023-11-30 11:01:11 -05:00
Upgrade : setting . UnifiedAlertingUpgradeSettings { } ,
2023-10-12 23:40:51 +01:00
} ,
}
service := NewTestMigrationService ( t , sqlStore , cfg )
2023-11-30 10:25:59 -05:00
err = service . migrationStore . SetCurrentAlertingType ( ctx , migrationStore . Legacy )
2023-10-12 23:40:51 +01:00
require . NoError ( t , err )
2023-11-30 10:25:59 -05:00
require . NoError ( t , service . Run ( ctx ) )
2023-10-12 23:40:51 +01:00
// Verify migration was run.
2023-11-30 10:25:59 -05:00
checkAlertingType ( t , ctx , service , migrationStore . UnifiedAlerting )
checkMigrationStatus ( t , ctx , service , 1 , true )
2023-10-12 23:40:51 +01:00
// Verify we created some folders.
2023-11-30 10:25:59 -05:00
newDashCount , err := x . Table ( "dashboard" ) . Count ( & dashboards . Dashboard { OrgID : 1 } )
2023-10-12 23:40:51 +01:00
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 {
2024-01-26 16:36:35 +01:00
createDashboard ( t , 99 , 1 , "dash-in-general-alerting-1" , generalAlertingFolder . UID , generalAlertingFolder . ID , nil ) ,
2023-10-12 23:40:51 +01:00
}
_ , err = x . Insert ( newDashes )
require . NoError ( t , err )
newF := getDashboard ( t , x , 1 , "dash-in-general-alerting-1" )
require . NotNil ( t , newF )
// Revert migration.
2023-11-30 10:25:59 -05:00
err = service . migrationStore . RevertAllOrgs ( context . Background ( ) )
2023-10-12 23:40:51 +01:00
require . NoError ( t , err )
2023-11-30 10:25:59 -05:00
// Verify revert was run. Should only set migration status for org.
checkAlertingType ( t , ctx , service , migrationStore . Legacy )
checkMigrationStatus ( t , ctx , service , 1 , false )
2023-10-12 23:40:51 +01:00
// Verify we are back to the original count + 2.
2023-11-30 10:25:59 -05:00
newDashCount , err = x . Table ( "dashboard" ) . Count ( & dashboards . Dashboard { OrgID : 1 } )
2023-10-12 23:40:51 +01:00
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 ) )
}
} )
2023-11-30 10:25:59 -05:00
2023-11-30 11:01:11 -05:00
t . Run ( "CleanUpgrade story" , func ( t * testing . T ) {
2023-11-30 10:25:59 -05:00
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 )
2023-11-30 11:01:11 -05:00
// Disable UA.
// This run should just set migration status to false.
2023-11-30 10:25:59 -05:00
service . cfg . UnifiedAlerting . Enabled = pointer ( false )
2023-11-30 11:01:11 -05:00
require . NoError ( t , service . Run ( ctx ) )
checkAlertingType ( t , ctx , service , migrationStore . Legacy )
2023-11-30 10:25:59 -05:00
checkMigrationStatus ( t , ctx , service , 1 , true )
checkAlertRulesCount ( t , x , 1 , 1 )
2023-11-30 11:01:11 -05:00
// 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.
2023-11-30 10:25:59 -05:00
// This run should not revert UA data.
service . cfg . UnifiedAlerting . Enabled = pointer ( false )
2023-11-30 11:01:11 -05:00
service . cfg . UnifiedAlerting . Upgrade . CleanUpgrade = true
2023-11-30 10:25:59 -05:00
require . NoError ( t , service . Run ( ctx ) )
checkAlertingType ( t , ctx , service , migrationStore . Legacy )
2023-11-30 11:01:11 -05:00
checkMigrationStatus ( t , ctx , service , 1 , true )
checkAlertRulesCount ( t , x , 1 , 1 ) // Still 1
2023-11-30 10:25:59 -05:00
2023-11-30 11:01:11 -05:00
// Enable UA with clean flag.
// This run should revert and remigrate org, new alert is migrated.
2023-11-30 10:25:59 -05:00
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
2023-11-30 11:01:11 -05:00
// 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 )
2023-11-30 10:25:59 -05:00
} )
}
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" )
2023-10-12 13:43:10 +01:00
}
2024-01-05 18:19:12 -05:00
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
2024-01-30 18:26:34 +02:00
d1Copy . FolderUID = f2 . UID
2024-01-05 18:19:12 -05:00
_ , 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 {
2024-01-26 16:36:35 +01:00
d := createDashboard ( h . t , h . dashIncr , 1 , fmt . Sprintf ( "dash%d" , h . dashIncr ) , folder . UID , folder . ID , nil )
2024-01-05 18:19:12 -05:00
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 ,
}
}