Dashboard V0->V1 Migration: Schema migration v36 (#100757)

---------

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>
This commit is contained in:
Ivan Ortega Alba
2025-06-16 15:53:41 +02:00
committed by GitHub
co-authored by Todd Treece Haris Rozajac Stephanie Hingtgen
parent 6767adb4a6
commit 59c2f15433
17 changed files with 2977 additions and 15 deletions
@@ -1,20 +1,38 @@
package schemaversion
import "strconv"
type SchemaVersionMigrationFunc func(map[string]interface{}) error
import (
"strconv"
)
const (
MIN_VERSION = 36
MIN_VERSION = 35
LATEST_VERSION = 41
)
var Migrations = map[int]SchemaVersionMigrationFunc{
37: V37,
38: V38,
39: V39,
40: V40,
41: V41,
type SchemaVersionMigrationFunc func(map[string]interface{}) error
type DataSourceInfo struct {
Default bool
UID string
Name string
Type string
ID int64
APIVersion string
}
type DataSourceInfoProvider interface {
GetDataSourceInfo() []DataSourceInfo
}
func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc {
return map[int]SchemaVersionMigrationFunc{
36: V36(dsInfoProvider),
37: V37,
38: V38,
39: V39,
40: V40,
41: V41,
}
}
func GetSchemaVersion(dash map[string]interface{}) int {
@@ -0,0 +1,220 @@
package schemaversion
// V36 migrates dashboard datasource references from string names to UIDs.
// This migration converts datasource references in annotations, template variables, and panels
// from the old format (string name or UID) to the new format (object with uid, type, apiVersion).
// This matches the frontend migration in DashboardMigrator.ts.
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
}
}
// getDataSourceRef creates a datasource reference object with uid, type and optional apiVersion
func getDataSourceRef(ds *DataSourceInfo) map[string]interface{} {
if ds == nil {
return nil
}
ref := map[string]interface{}{
"uid": ds.UID,
"type": ds.Type,
}
if ds.APIVersion != "" {
ref["apiVersion"] = ds.APIVersion
}
return ref
}
// getDefaultDSInstanceSettings returns the default datasource if one exists
func getDefaultDSInstanceSettings(datasources []DataSourceInfo) *DataSourceInfo {
for _, ds := range datasources {
if ds.Default {
return &DataSourceInfo{
UID: ds.UID,
Type: ds.Type,
Name: ds.Name,
APIVersion: ds.APIVersion,
}
}
}
return nil
}
// getInstanceSettings looks up a datasource by name or uid reference
func getInstanceSettings(nameOrRef interface{}, datasources []DataSourceInfo) *DataSourceInfo {
if nameOrRef == nil || nameOrRef == "default" {
return getDefaultDSInstanceSettings(datasources)
}
for _, ds := range datasources {
if str, ok := nameOrRef.(string); ok {
if str == ds.Name || str == ds.UID {
return &DataSourceInfo{
UID: ds.UID,
Type: ds.Type,
Name: ds.Name,
APIVersion: ds.APIVersion,
}
}
}
if ref, ok := nameOrRef.(map[string]interface{}); ok {
if uid, hasUID := ref["uid"]; hasUID {
if uid == ds.UID {
return &DataSourceInfo{
UID: ds.UID,
Type: ds.Type,
Name: ds.Name,
APIVersion: ds.APIVersion,
}
}
}
}
}
return nil
}
// migrateDatasourceNameToRef converts a datasource name/uid string to a reference object
// Matches the frontend migrateDatasourceNameToRef function in DashboardMigrator.ts
func migrateDatasourceNameToRef(nameOrRef interface{}, options map[string]bool, datasources []DataSourceInfo) map[string]interface{} {
if options["returnDefaultAsNull"] && (nameOrRef == nil || nameOrRef == "default") {
return nil
}
if dsRef, ok := nameOrRef.(map[string]interface{}); ok {
if _, hasUID := dsRef["uid"]; hasUID {
return dsRef
}
}
ds := getInstanceSettings(nameOrRef, datasources)
if ds != nil {
return getDataSourceRef(ds)
}
if dsName, ok := nameOrRef.(string); ok && dsName != "" {
return map[string]interface{}{
"uid": dsName,
}
}
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
}
if ds, exists := queryMap["datasource"]; exists {
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
}
for _, variable := range list {
varMap, ok := variable.(map[string]interface{})
if !ok {
continue
}
if varType, ok := varMap["type"].(string); ok && varType == "query" {
if ds, exists := varMap["datasource"]; exists {
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) {
if panels, ok := dashboard["panels"].([]interface{}); ok {
for _, panel := range panels {
if panelMap, ok := panel.(map[string]interface{}); ok {
migratePanelDatasources(panelMap, datasources)
}
}
}
}
// migratePanelDatasources updates datasource references in a single panel and its targets
func migratePanelDatasources(panelMap map[string]interface{}, datasources []DataSourceInfo) {
if targets, hasTargets := panelMap["targets"].([]interface{}); hasTargets && len(targets) > 0 {
panelDataSourceWasDefault := false
// Handle panel datasource
if ds, exists := panelMap["datasource"]; exists {
if ds == nil {
defaultDS := getDefaultDSInstanceSettings(datasources)
panelMap["datasource"] = getDataSourceRef(defaultDS)
panelDataSourceWasDefault = true
} else {
panelMap["datasource"] = migrateDatasourceNameToRef(ds, map[string]bool{"returnDefaultAsNull": true}, datasources)
}
}
// Handle target datasources
for _, target := range targets {
if targetMap, ok := target.(map[string]interface{}); ok {
ds, exists := targetMap["datasource"]
// Check if target datasource is null or has no uid
isNullOrNoUID := !exists || ds == nil
if !isNullOrNoUID {
if dsMap, ok := ds.(map[string]interface{}); ok {
if uid, hasUID := dsMap["uid"]; !hasUID || uid == nil {
isNullOrNoUID = true
}
}
}
if isNullOrNoUID {
// If panel doesn't have mixed datasource, use panel's datasource
if panelDS, ok := panelMap["datasource"].(map[string]interface{}); ok {
if uid, hasUID := panelDS["uid"].(string); hasUID && uid != "-- Mixed --" {
targetMap["datasource"] = panelDS
}
}
} else {
// Migrate existing target datasource
targetDS := migrateDatasourceNameToRef(ds, map[string]bool{"returnDefaultAsNull": false}, datasources)
targetMap["datasource"] = targetDS
}
// Update panel datasource if it was default and target is not an expression
if panelDataSourceWasDefault {
if targetDS, ok := targetMap["datasource"].(map[string]interface{}); ok {
if uid, ok := targetDS["uid"].(string); ok && uid != "__expr__" {
panelMap["datasource"] = targetDS
}
}
}
}
}
}
}
@@ -0,0 +1,571 @@
package schemaversion_test
import (
"testing"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil"
)
func TestV36(t *testing.T) {
// Pass the mock provider to V36
migration := schemaversion.V36(testutil.GetTestProvider())
tests := []migrationTestCase{
{
name: "dashboard with no datasources",
input: map[string]interface{}{
"schemaVersion": 35,
},
expected: map[string]interface{}{
"schemaVersion": 36,
},
},
{
name: "panel with null datasource should get default datasource",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": nil,
"targets": []interface{}{
map[string]interface{}{},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
},
},
},
},
},
},
{
name: "panel with mixed datasources should preserve target datasources",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "-- Mixed --",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": "Elasticsearch",
},
map[string]interface{}{
"datasource": "other-ds",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "-- Mixed --",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
},
{
name: "panel with specific datasource should apply to targets without datasource",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": "Default",
"targets": []interface{}{
map[string]interface{}{
"datasource": nil,
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": nil,
},
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
},
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
},
},
},
},
},
},
{
name: "dashboard with annotations using default datasource",
input: map[string]interface{}{
"schemaVersion": 35,
"annotations": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"datasource": "Default",
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"annotations": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
},
},
},
},
},
{
name: "dashboard with annotations using non-default datasource by UID",
input: map[string]interface{}{
"schemaVersion": 35,
"annotations": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"datasource": "other-ds",
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"annotations": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
{
name: "dashboard with annotations using non-default datasource by name",
input: map[string]interface{}{
"schemaVersion": 35,
"annotations": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"datasource": "Elasticsearch",
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"annotations": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
{
name: "dashboard with template variables using default datasource",
input: map[string]interface{}{
"schemaVersion": 35,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"datasource": nil,
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
},
},
},
},
},
{
name: "dashboard with template variables using non-default datasource by UID",
input: map[string]interface{}{
"schemaVersion": 35,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"datasource": "other-ds",
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
{
name: "dashboard with template variables using non-default datasource by name",
input: map[string]interface{}{
"schemaVersion": 35,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"datasource": "Elasticsearch",
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"templating": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"type": "query",
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
{
name: "dashboard with panels using default datasource",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": "Default",
"targets": []interface{}{
map[string]interface{}{
"datasource": "Default",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
},
},
},
},
},
},
{
name: "dashboard with panels using non-default datasource by UID",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": "other-ds",
"targets": []interface{}{
map[string]interface{}{
"datasource": "other-ds",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
},
{
name: "dashboard with panels using non-default datasource by name",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": "Elasticsearch",
"targets": []interface{}{
map[string]interface{}{
"datasource": "Elasticsearch",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
},
{
name: "dashboard with mixed panel and target datasources",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": "Default",
"targets": []interface{}{
map[string]interface{}{
"datasource": "Elasticsearch",
},
map[string]interface{}{
"datasource": "other-ds",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
},
{
name: "panel with null datasource and expression queries should get default datasource",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": nil,
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
},
map[string]interface{}{
"refId": "B",
"datasource": map[string]interface{}{
"uid": "__expr__",
},
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-ds",
"apiVersion": "v1",
},
},
map[string]interface{}{
"refId": "B",
"datasource": map[string]interface{}{
"uid": "__expr__",
},
},
},
},
},
},
},
{
name: "panel with null datasource should inherit from query datasource",
input: map[string]interface{}{
"schemaVersion": 35,
"panels": []interface{}{
map[string]interface{}{
"datasource": nil,
"targets": []interface{}{
map[string]interface{}{
"datasource": "Elasticsearch",
},
},
},
},
},
expected: map[string]interface{}{
"schemaVersion": 36,
"panels": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "other-ds",
"apiVersion": "v2",
},
},
},
},
},
},
},
}
runMigrationTests(t, tests, migration)
}