Schema: convert dashboards from v1beta1 to v2beta1 (#109037)

- Implement full conversion pipeline from v1beta1 → v2beta1
- Ensure frontend–backend parity for all dashboard serialization paths
- Add automatic data loss detection for conversions (panels, queries, annotations, links, variables)
- Extract atomic conversion functions for v0 → v1beta1 → v2alpha1 → v2beta1
- Introduce conversion metrics and detailed logging for loss tracking
- Normalize datasource resolution, defaults, and annotation processing
- Improve panel layout serialization and y-coordinate normalization
- Fix inconsistencies in nested panels and collapsed row behavior
- Refine variable handling:
  - Filter refId from variable query specs
  - Default variable refresh to 'never' (matches frontend)
  - Fix constant and interval variable handling for missing queries
- Unify schema defaults (enable, hide, iconColor, editable, liveNow)
- Fix pluginId usage (UID vs type) and datasource references
- Fix metrics.go bug swallowing errors (return nil → return err)
- Add tests for version-specific conversion error handling
- Add data loss detection tests using source/target version comparison
- Clean up lint issues, legacy code, and redundant files
- Update OpenAPI snapshots and migrated dashboards
- Improve backend migrator to reuse datasource provider and match frontend logic

Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
This commit is contained in:
Ivan Ortega Alba
2025-11-12 11:43:46 +01:00
committed by GitHub
co-authored by Haris Rozajac Oscar Kilhed Stephanie Hingtgen
parent 09942c08db
commit e463781077
273 changed files with 75003 additions and 588 deletions
@@ -0,0 +1,392 @@
package conversion
import (
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// TestV0ConversionErrorHandling tests that v0 conversion functions properly return errors and set status
func TestV0ConversionErrorHandling(t *testing.T) {
// Initialize the migrator with a test data source provider
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
migration.Initialize(dsProvider)
tests := []struct {
name string
source *dashv0.Dashboard
target interface{}
expectError bool
expectStatusSet bool
checkStatus func(t *testing.T, target interface{})
}{
{
name: "Convert_V0_to_V1beta1 returns error on migration failure",
source: &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version that will cause migration to fail
},
},
},
target: &dashv1.Dashboard{},
expectError: true,
expectStatusSet: true,
checkStatus: func(t *testing.T, target interface{}) {
out := target.(*dashv1.Dashboard)
require.NotNil(t, out.Status.Conversion)
require.True(t, out.Status.Conversion.Failed)
require.NotNil(t, out.Status.Conversion.Error)
},
},
{
name: "Convert_V0_to_V2alpha1 sets status on first step migration failure",
source: &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version that will cause migration to fail
},
},
},
target: &dashv2alpha1.Dashboard{},
expectError: false, // Convert_V0_to_V2alpha1 doesn't return error, just sets status
expectStatusSet: true,
checkStatus: func(t *testing.T, target interface{}) {
out := target.(*dashv2alpha1.Dashboard)
// Status should be set when first step fails
require.NotNil(t, out.Status.Conversion)
require.True(t, out.Status.Conversion.Failed)
require.NotNil(t, out.Status.Conversion.Error)
},
},
{
name: "Convert_V0_to_V2beta1 returns error on first step migration failure",
source: &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version that will cause migration to fail
},
},
},
target: &dashv2beta1.Dashboard{},
expectError: true,
expectStatusSet: true,
checkStatus: func(t *testing.T, target interface{}) {
out := target.(*dashv2beta1.Dashboard)
require.NotNil(t, out.Status.Conversion)
require.True(t, out.Status.Conversion.Failed)
require.NotNil(t, out.Status.Conversion.Error)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var err error
switch target := tt.target.(type) {
case *dashv1.Dashboard:
err = Convert_V0_to_V1beta1(tt.source, target, nil)
case *dashv2alpha1.Dashboard:
err = Convert_V0_to_V2alpha1(tt.source, target, nil, dsProvider)
case *dashv2beta1.Dashboard:
err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider)
default:
t.Fatalf("unexpected target type: %T", target)
}
if tt.expectError {
require.Error(t, err, "expected conversion to return error")
}
if tt.expectStatusSet {
tt.checkStatus(t, tt.target)
}
})
}
}
// TestV0ConversionErrorPropagation tests that errors from atomic functions are properly propagated
func TestV0ConversionErrorPropagation(t *testing.T) {
// Initialize the migrator with a test data source provider
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
migration.Initialize(dsProvider)
t.Run("ConvertDashboard_V0_to_V1beta1 returns error on migration failure", func(t *testing.T) {
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version that will cause migration to fail
},
},
}
target := &dashv1.Dashboard{}
err := ConvertDashboard_V0_to_V1beta1(source, target, nil)
require.Error(t, err, "expected error on migration failure")
require.NotNil(t, target.Status.Conversion)
require.True(t, target.Status.Conversion.Failed)
require.NotNil(t, target.Status.Conversion.Error)
})
t.Run("Convert_V0_to_V1beta1 propagates error from ConvertDashboard_V0_to_V1beta1", func(t *testing.T) {
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version that will cause migration to fail
},
},
}
target := &dashv1.Dashboard{}
err := Convert_V0_to_V1beta1(source, target, nil)
require.Error(t, err, "expected error to be returned")
require.NotNil(t, target.Status.Conversion)
require.True(t, target.Status.Conversion.Failed)
require.NotNil(t, target.Status.Conversion.Error)
})
t.Run("Convert_V0_to_V2beta1 returns error on first step migration failure", func(t *testing.T) {
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version that will cause migration to fail
},
},
}
target := &dashv2beta1.Dashboard{}
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
require.Error(t, err, "expected error to be returned on first step failure")
require.NotNil(t, target.Status.Conversion)
require.True(t, target.Status.Conversion.Failed)
require.NotNil(t, target.Status.Conversion.Error)
})
}
// TestV0ConversionSuccessPaths tests that successful conversion paths are covered
func TestV0ConversionSuccessPaths(t *testing.T) {
// Initialize the migrator with a test data source provider
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
migration.Initialize(dsProvider)
t.Run("Convert_V0_to_V1beta1 success path returns nil", func(t *testing.T) {
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 42, // Latest schema version - no migration needed
},
},
}
target := &dashv1.Dashboard{}
err := Convert_V0_to_V1beta1(source, target, nil)
require.NoError(t, err, "expected successful conversion")
require.NotNil(t, target.Status.Conversion)
require.False(t, target.Status.Conversion.Failed)
})
t.Run("Convert_V0_to_V2alpha1 success path returns nil", func(t *testing.T) {
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 42, // Latest schema version - no migration needed
},
},
}
target := &dashv2alpha1.Dashboard{}
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider)
require.NoError(t, err, "expected successful conversion")
// Layout should be set even on success
require.NotNil(t, target.Spec.Layout.GridLayoutKind)
})
t.Run("Convert_V0_to_V2beta1 success path returns nil", func(t *testing.T) {
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 42, // Latest schema version - no migration needed
},
},
}
target := &dashv2beta1.Dashboard{}
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
require.NoError(t, err, "expected successful conversion")
})
}
// TestV0ConversionSecondStepErrors tests error handling in second step of multi-step conversions
func TestV0ConversionSecondStepErrors(t *testing.T) {
// Initialize the migrator with a test data source provider
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
migration.Initialize(dsProvider)
t.Run("Convert_V0_to_V2alpha1 sets status on first step error", func(t *testing.T) {
// Create a dashboard that will fail v0->v1beta1 conversion
// Use schemaVersion 0 which will cause migration to fail
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version that will cause migration to fail
},
},
}
target := &dashv2alpha1.Dashboard{}
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider)
// Convert_V0_to_V2alpha1 doesn't return error, just sets status
require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error")
// Status should be set when first step fails
require.NotNil(t, target.Status.Conversion, "Status should be set on first step error")
require.True(t, target.Status.Conversion.Failed, "Failed should be true")
require.NotNil(t, target.Status.Conversion.Error, "Error should be set")
require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion)
})
t.Run("Convert_V0_to_V2alpha1 sets status on second step error", func(t *testing.T) {
// Create a dashboard that will pass v0->v1beta1 but fail v1beta1->v2alpha1
// We need to create invalid JSON structure that will cause JSON marshaling to fail
// or create a dashboard with invalid structure that causes transformation to fail
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 42, // Valid schema version that will pass first step
"dashboard": map[string]interface{}{
"title": "test",
// Create invalid structure that will cause JSON marshaling to fail
// by using a channel which cannot be marshaled
"invalidField": make(chan int), // This will cause JSON marshal to fail
},
},
},
}
target := &dashv2alpha1.Dashboard{}
err := Convert_V0_to_V2alpha1(source, target, nil, dsProvider)
// Convert_V0_to_V2alpha1 doesn't return error, just sets status
require.NoError(t, err, "Convert_V0_to_V2alpha1 doesn't return error")
// If second step fails, status should be set
// Note: The error might occur in first step if JSON marshal fails early,
// but we're testing that the error handling path exists
if target.Status.Conversion != nil && target.Status.Conversion.Failed {
require.True(t, target.Status.Conversion.Failed)
require.NotNil(t, target.Status.Conversion.Error)
require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion)
}
})
t.Run("Convert_V0_to_V2beta1 returns error on second step failure", func(t *testing.T) {
// Create a scenario where first step succeeds but second step fails
// This is harder to trigger, but we can test the code path exists
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 25, // Valid schema version that will pass first step
},
},
}
target := &dashv2beta1.Dashboard{}
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
// May or may not error depending on dashboard content
// But if it does error on second step, status should be set
if err != nil {
require.NotNil(t, target.Status.Conversion)
require.True(t, target.Status.Conversion.Failed)
require.NotNil(t, target.Status.Conversion.Error)
}
})
t.Run("Convert_V0_to_V2beta1 returns error on third step failure", func(t *testing.T) {
// Create a scenario where first two steps succeed but third step fails
source := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 25, // Valid schema version that will pass first two steps
},
},
}
target := &dashv2beta1.Dashboard{}
err := Convert_V0_to_V2beta1(source, target, nil, dsProvider)
// May or may not error depending on dashboard content
// But if it does error on third step, status should be set
if err != nil {
require.NotNil(t, target.Status.Conversion)
require.True(t, target.Status.Conversion.Failed)
require.NotNil(t, target.Status.Conversion.Error)
}
})
}