Files
grafana/apps/dashboard/pkg/migration/migrate.go
T
2025-11-18 09:21:05 +01:00

208 lines
7.0 KiB
Go

package migration
import (
"context"
"fmt"
"sync"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
// Initialize provides the migrator singleton with required dependencies and builds the map of migrations.
func Initialize(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) {
migratorInstance.init(dsIndexProvider, leIndexProvider)
}
// GetDataSourceIndexProvider returns the datasource index provider instance that was initialized.
func GetDataSourceIndexProvider() schemaversion.DataSourceIndexProvider {
// Wait for initialization to complete
<-migratorInstance.ready
return migratorInstance.dsIndexProvider
}
// GetLibraryElementIndexProvider returns the library element index provider instance that was initialized.
func GetLibraryElementIndexProvider() schemaversion.LibraryElementIndexProvider {
// Wait for initialization to complete
<-migratorInstance.ready
return migratorInstance.leIndexProvider
}
// ResetForTesting resets the migrator singleton for testing purposes.
func ResetForTesting() {
migratorInstance = &migrator{
migrations: map[int]schemaversion.SchemaVersionMigrationFunc{},
ready: make(chan struct{}),
dsIndexProvider: nil,
leIndexProvider: nil,
}
initOnce = sync.Once{}
}
// Migrate migrates the given dashboard to the target version.
// This will block until the migrator is initialized.
func Migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error {
return migratorInstance.migrate(ctx, dash, targetVersion)
}
var (
migratorInstance = &migrator{
migrations: map[int]schemaversion.SchemaVersionMigrationFunc{},
ready: make(chan struct{}),
}
initOnce sync.Once
)
type migrator struct {
ready chan struct{}
migrations map[int]schemaversion.SchemaVersionMigrationFunc
dsIndexProvider schemaversion.DataSourceIndexProvider
leIndexProvider schemaversion.LibraryElementIndexProvider
}
func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) {
initOnce.Do(func() {
m.dsIndexProvider = dsIndexProvider
m.leIndexProvider = leIndexProvider
m.migrations = schemaversion.GetMigrations(dsIndexProvider, leIndexProvider)
close(m.ready)
})
}
func (m *migrator) migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error {
if dash == nil {
return schemaversion.NewMigrationError("dashboard is nil", 0, targetVersion, "")
}
// wait for the migrator to be initialized
<-m.ready
// 0. Clean up dashboard properties that frontend never includes in save model
// These properties are added by backend but frontend filters them out
delete(dash, "__elements")
delete(dash, "__inputs")
delete(dash, "__requires")
// 1. Track which panels had transformations in original input (before any defaults applied)
// This is needed to match frontend hasOwnProperty behavior
trackOriginalTransformations(dash)
// 1.1. Track which panels had fieldConfig.defaults.custom in original input
// This is needed to preserve empty custom objects that were originally present
trackOriginalFieldConfigCustom(dash)
// 2. Apply ALL frontend defaults FIRST (DashboardModel + PanelModel defaults)
// This replicates the behavior of the frontend DashboardModel and PanelModel constructors
applyFrontendDefaults(dash)
// 2. Apply panel defaults to ALL panels (both top-level and nested in rows)
// The frontend creates PanelModel instances for all panels, including those in rows
if dashboardPanels, ok := dash["panels"].([]interface{}); ok {
for _, panelInterface := range dashboardPanels {
if panel, ok := panelInterface.(map[string]interface{}); ok {
applyPanelDefaults(panel)
}
}
}
// Apply defaults to panels inside rows (for pre-v16 dashboards)
// Match frontend upgradeToGridLayout: only panels NOT in collapsed rows get new PanelModel() constructor
if rows, ok := dash["rows"].([]interface{}); ok {
showRows := shouldShowRows(rows)
for _, rowInterface := range rows {
row, ok := rowInterface.(map[string]interface{})
if !ok {
continue
}
applyRowPanelDefaults(row, showRows)
}
}
// 3. Ensure panel IDs are unique for ALL panels (including nested ones)
// This matches the frontend ensurePanelsHaveUniqueIds() behavior
ensurePanelsHaveUniqueIds(dash)
// TODO: Probably we can check if we can migrate at the beginning of the function
// 4. Ensure schema version is set and if not default to 0
inputVersion := schemaversion.GetSchemaVersion(dash)
dash["schemaVersion"] = inputVersion
// If the schema version is older than the minimum version, with migration support,
// we don't migrate the dashboard.
if inputVersion < schemaversion.MIN_VERSION {
return schemaversion.NewMinimumVersionError(inputVersion)
}
// 5. Run existing migration pipeline UNCHANGED
// (All the existing v28, v29, etc. migrators run exactly as before)
for nextVersion := inputVersion + 1; nextVersion <= targetVersion; nextVersion++ {
if migration, ok := m.migrations[nextVersion]; ok {
if err := migration(ctx, dash); err != nil {
functionName := fmt.Sprintf("V%d", nextVersion)
return schemaversion.NewMigrationError("migration failed: "+err.Error(), inputVersion, nextVersion, functionName)
}
dash["schemaVersion"] = nextVersion
}
}
// 6. Add built-in annotation query after all migrations are complete
// This matches the frontend DashboardModel constructor behavior
addBuiltInAnnotationQuery(dash)
// 7. Clean up the dashboard to match frontend getSaveModel behavior
// This removes properties that shouldn't be persisted and filters out default values
cleanupDashboardForSave(dash)
if schemaversion.GetSchemaVersion(dash) != targetVersion {
return schemaversion.NewMigrationError("schema version not migrated to target version", inputVersion, targetVersion, "")
}
return nil
}
// shouldShowRows determines if row panels will be created (showRows logic)
func shouldShowRows(rows []interface{}) bool {
for _, rowInterface := range rows {
row, ok := rowInterface.(map[string]interface{})
if !ok {
continue
}
collapse := schemaversion.GetBoolValue(row, "collapse")
showTitle := schemaversion.GetBoolValue(row, "showTitle")
repeat := schemaversion.GetStringValue(row, "repeat")
if collapse || showTitle || repeat != "" {
return true
}
}
return false
}
// applyRowPanelDefaults applies panel defaults to panels within a row based on frontend logic
func applyRowPanelDefaults(row map[string]interface{}, showRows bool) {
rowPanels, ok := row["panels"].([]interface{})
if !ok {
return
}
collapse := schemaversion.GetBoolValue(row, "collapse")
// Frontend: if (rowPanelModel && rowPanel.collapsed) { push(panel) } else { push(new PanelModel(panel)) }
// Only non-collapsed panels get PanelModel defaults (refId: "A", overrides: [], etc.)
applyDefaults := !showRows || !collapse
if !applyDefaults {
return
}
for _, panelInterface := range rowPanels {
panel, ok := panelInterface.(map[string]interface{})
if !ok {
continue
}
applyPanelDefaults(panel)
}
}