93c14c52da
* wip: trying to understand how to get the ds info from migrator * add datasource info provider * Use DS service to fetch DS data * add more tests cases to match with migrator cases * Add snapshots * Non-existing DS * Add different DS for snapshots * fix import * Fix tests: guard against double initialization * don't use full datasource package in test * min version should be 35 * fix test * fix conversion test * Dashboards: Support schemaVersion v35 migration in backend * Dashboards: Support schemaVersion v34 migration in backend * Dashboards: Support schemaVersion v33 migration in backend * Apply suggestions from code review Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com> * Apply feedback * Remove unused parameters * Refactor to follow Go patterns * Update logic * Only write final migration result as output * Compare backend and frontend results * Improve snapshots to cover all possible use cases * Linter * wip make it consistent v33 * apply feedback * Return default when the ref cannot be found * Update apps/dashboard/pkg/migration/schemaversion/v33.go Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com> * apply feedback * Use same mocks backend/frontend * restore migrations * update snapshots * Adapt migration tests to use min versions * Ensure v40-v41 works * Ensure v39-v40 works * Simplify the naming of the files * adjust jest to new input convention * Ensure every migration v36-v41 works * Improve v38 naming * Ensure v36 migrates correctly * Skip v36 refs migrations on rows * Treat rows as frontend and ensure same results for v36 * Ensure v34 runs with the same logic than the frontend * Leave empty stadistics as valid option * ensure v33 is working as the frontend * Update tests * Undo frontend changes for legend handling * Remove filtering by version in the frontend * linter * Clean up v33 input JSON --------- Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com> Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
269 lines
8.2 KiB
Go
269 lines
8.2 KiB
Go
package schemaversion
|
|
|
|
// V36 migrates dashboard datasource references from legacy string format to structured UID-based objects.
|
|
//
|
|
// This migration addresses a critical evolution in Grafana's datasource architecture where datasource
|
|
// identification shifted from potentially ambiguous display names to reliable UIDs. The original format
|
|
// used string references that could break when datasources were renamed, moved between organizations,
|
|
// or when multiple datasources shared similar names. This created reliability and portability issues
|
|
// for dashboard sharing and automation workflows.
|
|
//
|
|
// The migration works by:
|
|
// 1. Processing annotations, template variables, and panels (including nested panels in rows)
|
|
// 2. Converting string datasource references to structured objects containing uid, type, and apiVersion
|
|
// 3. Handling null/missing datasource references by setting appropriate defaults
|
|
// 4. Maintaining consistency between panel and target datasource configurations
|
|
// 5. Preserving special datasource types like Mixed datasources and expression queries
|
|
//
|
|
// This transformation provides several critical benefits:
|
|
// - Eliminates datasource reference breakage when datasources are renamed
|
|
// - Enables reliable dashboard export/import across different Grafana instances
|
|
// - Supports advanced datasource features that require type and version information
|
|
// - Prepares the schema for future datasource management enhancements
|
|
// - Maintains backward compatibility while establishing a robust foundation
|
|
//
|
|
// The migration handles complex scenarios including:
|
|
// - Panels with missing datasource configuration (set to default)
|
|
// - Mixed datasource panels with heterogeneous targets
|
|
// - Expression queries that reference other queries
|
|
// - Template variables that depend on datasource queries
|
|
// - Annotation queries from various datasource types
|
|
//
|
|
// Example transformations:
|
|
//
|
|
// Before migration (string reference):
|
|
//
|
|
// datasource: "prometheus-prod"
|
|
// // or
|
|
// datasource: null
|
|
//
|
|
// After migration (structured object):
|
|
//
|
|
// datasource: {
|
|
// uid: "prometheus-uid-123",
|
|
// type: "prometheus",
|
|
// apiVersion: "v1"
|
|
// }
|
|
//
|
|
// Before migration (panel with targets):
|
|
//
|
|
// panel: {
|
|
// datasource: "CloudWatch",
|
|
// targets: [{
|
|
// datasource: null,
|
|
// refId: "A"
|
|
// }]
|
|
// }
|
|
//
|
|
// After migration (consistent references):
|
|
//
|
|
// panel: {
|
|
// datasource: {
|
|
// uid: "cloudwatch-uid-456",
|
|
// type: "cloudwatch",
|
|
// apiVersion: "v1"
|
|
// },
|
|
// targets: [{
|
|
// datasource: {
|
|
// uid: "cloudwatch-uid-456",
|
|
// type: "cloudwatch",
|
|
// apiVersion: "v1"
|
|
// },
|
|
// refId: "A"
|
|
// }]
|
|
// }
|
|
func V36(dsInfo DataSourceInfoProvider) SchemaVersionMigrationFunc {
|
|
datasources := dsInfo.GetDataSourceInfo()
|
|
return func(dashboard map[string]interface{}) error {
|
|
dashboard["schemaVersion"] = int(36)
|
|
|
|
migrateAnnotations(dashboard, datasources)
|
|
migrateTemplateVariables(dashboard, datasources)
|
|
migratePanels(dashboard, datasources)
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// migrateAnnotations updates datasource references in dashboard annotations
|
|
func migrateAnnotations(dashboard map[string]interface{}, datasources []DataSourceInfo) {
|
|
annotations, ok := dashboard["annotations"].(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
list, ok := annotations["list"].([]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for _, query := range list {
|
|
queryMap, ok := query.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Always migrate datasource, even if it doesn't exist (will be set to default)
|
|
ds := queryMap["datasource"]
|
|
queryMap["datasource"] = MigrateDatasourceNameToRef(ds, map[string]bool{"returnDefaultAsNull": false}, datasources)
|
|
}
|
|
}
|
|
|
|
// migrateTemplateVariables updates datasource references in dashboard variables
|
|
func migrateTemplateVariables(dashboard map[string]interface{}, datasources []DataSourceInfo) {
|
|
templating, ok := dashboard["templating"].(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
list, ok := templating["list"].([]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
defaultDS := GetDefaultDSInstanceSettings(datasources)
|
|
for _, variable := range list {
|
|
varMap, ok := variable.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
varType, ok := varMap["type"].(string)
|
|
if !ok || varType != "query" {
|
|
continue
|
|
}
|
|
|
|
ds, exists := varMap["datasource"]
|
|
// Handle null datasource variables by setting to default
|
|
if !exists || ds == nil {
|
|
varMap["datasource"] = GetDataSourceRef(defaultDS)
|
|
} else {
|
|
varMap["datasource"] = MigrateDatasourceNameToRef(ds, map[string]bool{"returnDefaultAsNull": false}, datasources)
|
|
}
|
|
}
|
|
}
|
|
|
|
// migratePanels updates datasource references in dashboard panels
|
|
func migratePanels(dashboard map[string]interface{}, datasources []DataSourceInfo) {
|
|
panels, ok := dashboard["panels"].([]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for _, panel := range panels {
|
|
panelMap, ok := panel.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
migratePanelDatasources(panelMap, datasources)
|
|
|
|
// Handle nested panels in collapsed rows
|
|
nestedPanels, hasNested := panelMap["panels"].([]interface{})
|
|
if !hasNested {
|
|
continue
|
|
}
|
|
|
|
for _, nestedPanel := range nestedPanels {
|
|
np, ok := nestedPanel.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
migratePanelDatasources(np, datasources)
|
|
}
|
|
}
|
|
}
|
|
|
|
// migratePanelDatasources updates datasource references in a single panel and its targets
|
|
func migratePanelDatasources(panelMap map[string]interface{}, datasources []DataSourceInfo) {
|
|
// NOTE: Even though row panels don't technically need datasource or targets fields,
|
|
// we process them anyway to exactly match frontend behavior and avoid inconsistencies
|
|
// between frontend and backend migrations. The frontend DashboardMigrator processes
|
|
// all panels uniformly without special row panel handling.
|
|
|
|
defaultDS := GetDefaultDSInstanceSettings(datasources)
|
|
panelDataSourceWasDefault := false
|
|
|
|
// Handle targets - treat empty arrays same as missing targets (matches frontend behavior)
|
|
targets, hasTargets := panelMap["targets"].([]interface{})
|
|
if !hasTargets || len(targets) == 0 {
|
|
targets = []interface{}{
|
|
map[string]interface{}{
|
|
"refId": "A",
|
|
},
|
|
}
|
|
panelMap["targets"] = targets
|
|
hasTargets = true
|
|
}
|
|
|
|
// Handle panel datasource
|
|
ds, exists := panelMap["datasource"]
|
|
if !exists || ds == nil {
|
|
// Set to default if panel has targets (matches frontend logic)
|
|
panelMap["datasource"] = GetDataSourceRef(defaultDS)
|
|
panelDataSourceWasDefault = true
|
|
} else {
|
|
// Migrate existing non-null datasource (should be null after V33)
|
|
migrated := MigrateDatasourceNameToRef(ds, map[string]bool{"returnDefaultAsNull": true}, datasources)
|
|
if migrated == nil {
|
|
// If migration returned nil, set to default
|
|
panelMap["datasource"] = GetDataSourceRef(defaultDS)
|
|
panelDataSourceWasDefault = true
|
|
} else {
|
|
panelMap["datasource"] = migrated
|
|
}
|
|
}
|
|
|
|
// Handle target datasources
|
|
if !hasTargets {
|
|
return
|
|
}
|
|
|
|
for _, target := range targets {
|
|
targetMap, ok := target.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
ds, exists := targetMap["datasource"]
|
|
|
|
// Check if target datasource is null, missing, or has no uid
|
|
needsDefault := false
|
|
if !exists || ds == nil {
|
|
needsDefault = true
|
|
} else if dsMap, ok := ds.(map[string]interface{}); ok {
|
|
uid, hasUID := dsMap["uid"]
|
|
if !hasUID || uid == nil {
|
|
needsDefault = true
|
|
}
|
|
}
|
|
|
|
if needsDefault {
|
|
// Use panel's datasource if it's not mixed
|
|
panelDS, ok := panelMap["datasource"].(map[string]interface{})
|
|
if ok {
|
|
uid, hasUID := panelDS["uid"].(string)
|
|
if hasUID && uid != "-- Mixed --" {
|
|
targetMap["datasource"] = panelDS
|
|
} else {
|
|
// If panel is mixed, migrate target datasource independently
|
|
targetMap["datasource"] = MigrateDatasourceNameToRef(ds, map[string]bool{"returnDefaultAsNull": false}, datasources)
|
|
}
|
|
}
|
|
} else {
|
|
// Migrate existing target datasource
|
|
targetMap["datasource"] = MigrateDatasourceNameToRef(ds, map[string]bool{"returnDefaultAsNull": false}, datasources)
|
|
}
|
|
|
|
// Update panel datasource if it was default and target is not an expression
|
|
if panelDataSourceWasDefault {
|
|
targetDS, ok := targetMap["datasource"].(map[string]interface{})
|
|
if ok {
|
|
uid, ok := targetDS["uid"].(string)
|
|
if ok && uid != "__expr__" {
|
|
panelMap["datasource"] = targetDS
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|