Files
grafana/apps/dashboard/pkg/migration/conversion/v0alpha1_to_v1beta1.go
T
Haris Rozajac 37c1e3fb02 Dashboard Schema v1beta1 to v2alpha1: Preserve string template variable datasource references in query variables (#115516)
* Dashboard migration: preserve legacy string datasource references

Fix v1beta1 → v2alpha1 conversion to handle legacy string datasource
references in QueryVariable, AdhocVariable, and GroupByVariable.

Previously, string datasource references (both template variables like
"$datasource" and direct names/UIDs like "prometheus") were being
dropped during conversion, causing variable chaining to break.

The frontend's DatasourceSrv.getInstanceSettings() already handles
string references by trying uid → name → id lookup at runtime, so we
preserve the string in the uid field and let the frontend resolve it.

* trigger frontend ci tests when dashboard migration code changes

* v1: if string convert to DS ref

* Update migration testdata to fix template variable datasource references

* update
2025-12-18 15:11:09 -07:00

137 lines
5.6 KiB
Go

package conversion
import (
"context"
"strings"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/utils/ptr"
"github.com/grafana/authlib/types"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
// prepareV0ConversionContext sets up the context with namespace and service identity
// for v0 dashboard conversions. This context is needed to retrieve datasources for migrating
// old dashboard schemas (these migrations used to be run in the frontend).
// A background service identity is used because the user who is reading the specific dashboard
// may not have access to all the datasources in the dashboard, but the migration still needs to take place
// in order to be able to convert between k8s versions (so that we have a guaranteed structure to convert between).
func prepareV0ConversionContext(in *dashv0.Dashboard) (context.Context, *types.NamespaceInfo, error) {
ctx := request.WithNamespace(context.Background(), in.GetNamespace())
nsInfo, err := types.ParseNamespace(in.GetNamespace())
if err != nil {
return nil, nil, err
}
ctx, _ = identity.WithServiceIdentity(ctx, nsInfo.OrgID)
return ctx, &nsInfo, nil
}
// migrateV0Dashboard migrates a v0 dashboard object to the target schema version.
// This is used to ensure the dashboard structure is consistent before converting between k8s API versions.
func migrateV0Dashboard(ctx context.Context, dashboardObject map[string]interface{}, targetVersion int) error {
if ctx == nil {
return schemaversion.NewMigrationError("context is nil", schemaversion.GetSchemaVersion(dashboardObject), targetVersion, "migrateV0Dashboard")
}
return migration.Migrate(ctx, dashboardObject, targetVersion)
}
// ConvertDashboard_V0_to_V1beta1 converts a v0alpha1 dashboard to v1beta1 format.
// This is an atomic single-step conversion that:
// 1. Migrates the dashboard to the latest schema version
// 2. Transforms the migrated dashboard to v1beta1 format
//
// the scope passed into this function is used in k8s apimachinery for migrations, but we also need the context
// to have what grafana expects in the request context, so that we can retrieve datasources for migrating
// some of the old dashboard schemas (these migrations used to be run in the frontend)
//
// a background service identity is used here because the user who is reading the specific dashboard
// may not have access to all the datasources in the dashboard, but the migration still needs to take place
// in order to be able to convert between k8s versions (so that we have a guaranteed structure to convert between)
func ConvertDashboard_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.APIVersion = dashv1.APIVERSION
out.Kind = in.Kind
out.Spec.Object = in.Spec.Object
out.Status = dashv1.DashboardStatus{
Conversion: &dashv1.DashboardConversionStatus{
StoredVersion: ptr.To(dashv0.VERSION),
},
}
ctx, _, err := prepareV0ConversionContext(in)
if err != nil {
out.Status.Conversion.Failed = true
out.Status.Conversion.Error = ptr.To(err.Error())
return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1")
}
if err := migrateV0Dashboard(ctx, out.Spec.Object, schemaversion.LATEST_VERSION); err != nil {
out.Status.Conversion.Failed = true
out.Status.Conversion.Error = ptr.To(err.Error())
return schemaversion.NewMigrationError(err.Error(), schemaversion.GetSchemaVersion(in.Spec.Object), schemaversion.LATEST_VERSION, "Convert_V0_to_V1")
}
// Normalize template variable datasources from string to object format
// This handles legacy dashboards where query variables have datasource: "$datasource" (string)
// instead of datasource: { uid: "$datasource" } (object)
// our migration pipeline in v36 doesn't address because this was not addressed historically
// in DashboardMigrator - see public/app/features/dashboard/state/DashboardMigrator.ts#L607
// Which means that we have schemaVersion: 42 dashboards where datasource variable references are still strings
normalizeTemplateVariableDatasources(out.Spec.Object)
return nil
}
// normalizeTemplateVariableDatasources converts template variable string datasources to object format.
// Legacy dashboards may have query variables with datasource: "$datasource" (string).
// This normalizes them to datasource: { uid: "$datasource" } for consistent V1→V2 conversion.
func normalizeTemplateVariableDatasources(dashboard map[string]interface{}) {
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
}
varType, _ := varMap["type"].(string)
if varType != "query" {
continue
}
ds := varMap["datasource"]
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
// Convert string template variable reference to object format
varMap["datasource"] = map[string]interface{}{
"uid": dsStr,
}
}
}
}
// isTemplateVariableRef checks if a string is a Grafana template variable reference.
// Template variables can be in the form: $varname or ${varname}
func isTemplateVariableRef(s string) bool {
if s == "" {
return false
}
return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${")
}