Files
grafana/apps/dashboard/pkg/migration/conversion/v0_test.go
T
Ivan Ortega Alba 8aab6302c5 Fix conversion error shallowed and normalize conversion status (#115086)
* Fix the conversion shallowed error and normalize the conversion status

* Add unit tests to ensure all permutations data loss detection

* Fix counting issue
2025-12-11 08:01:31 +00:00

568 lines
22 KiB
Go

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)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
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 returns error and 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: true, // Convert_V0_to_V2alpha1 now returns error for proper metrics/logging
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, leProvider)
case *dashv2beta1.Dashboard:
err = Convert_V0_to_V2beta1(tt.source, target, nil, dsProvider, leProvider)
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)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
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, leProvider)
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)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
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, leProvider)
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, leProvider)
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)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
t.Run("Convert_V0_to_V2alpha1 returns error and 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, leProvider)
// Convert_V0_to_V2alpha1 returns error for proper metrics/logging
require.Error(t, err, "Convert_V0_to_V2alpha1 should return error")
// Status should also 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 returns error and 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, leProvider)
// Convert_V0_to_V2alpha1 returns error for proper metrics/logging
require.Error(t, err, "Convert_V0_to_V2alpha1 should return error")
// Status should also be set
require.NotNil(t, target.Status.Conversion, "Status should be set on 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_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, leProvider)
// 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, leProvider)
// 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)
}
})
}
// TestV0ConversionConsistency_ErrorsMustBeReturned ensures v0 conversion functions
// return errors instead of swallowing them. This prevents metrics and logs from being silently dropped.
func TestV0ConversionConsistency_ErrorsMustBeReturned(t *testing.T) {
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
// Create a v0 dashboard that will fail conversion (invalid schema version)
invalidV0 := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
Name: "test-dashboard",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0, // Invalid schema version causes migration to fail
},
},
}
t.Run("Convert_V0_to_V1beta1 must return error on failure", func(t *testing.T) {
target := &dashv1.Dashboard{}
err := Convert_V0_to_V1beta1(invalidV0, target, nil)
require.Error(t, err, "Convert_V0_to_V1beta1 must return error, not swallow it")
require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true")
})
t.Run("Convert_V0_to_V2alpha1 must return error on failure", func(t *testing.T) {
target := &dashv2alpha1.Dashboard{}
err := Convert_V0_to_V2alpha1(invalidV0, target, nil, dsProvider, leProvider)
require.Error(t, err, "Convert_V0_to_V2alpha1 must return error, not swallow it")
require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true")
})
t.Run("Convert_V0_to_V2beta1 must return error on failure", func(t *testing.T) {
target := &dashv2beta1.Dashboard{}
err := Convert_V0_to_V2beta1(invalidV0, target, nil, dsProvider, leProvider)
require.Error(t, err, "Convert_V0_to_V2beta1 must return error, not swallow it")
require.True(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be true")
})
}
// TestV0ConversionConsistency_SuccessStatusMustBeSet ensures v0 conversion functions
// set Status.Conversion with Failed=false on successful conversion.
func TestV0ConversionConsistency_SuccessStatusMustBeSet(t *testing.T) {
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
// Create a valid v0 dashboard
validV0 := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test-dashboard",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 42,
},
},
}
t.Run("Convert_V0_to_V1beta1 must set success status", func(t *testing.T) {
target := &dashv1.Dashboard{}
err := Convert_V0_to_V1beta1(validV0, target, nil)
require.NoError(t, err)
require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success")
require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success")
require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set")
require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion)
})
t.Run("Convert_V0_to_V2alpha1 must set success status", func(t *testing.T) {
target := &dashv2alpha1.Dashboard{}
err := Convert_V0_to_V2alpha1(validV0, target, nil, dsProvider, leProvider)
require.NoError(t, err)
require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success")
require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success")
require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set")
require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion)
})
t.Run("Convert_V0_to_V2beta1 must set success status", func(t *testing.T) {
target := &dashv2beta1.Dashboard{}
err := Convert_V0_to_V2beta1(validV0, target, nil, dsProvider, leProvider)
require.NoError(t, err)
require.NotNil(t, target.Status.Conversion, "Status.Conversion must be set on success")
require.False(t, target.Status.Conversion.Failed, "Status.Conversion.Failed must be false on success")
require.NotNil(t, target.Status.Conversion.StoredVersion, "StoredVersion must be set")
require.Equal(t, dashv0.VERSION, *target.Status.Conversion.StoredVersion)
})
}
// TestV0ConversionConsistency_ObjectMetaMustBeSetOnError ensures v0 conversion functions
// set ObjectMeta, APIVersion, and Kind on the target even when conversion fails.
func TestV0ConversionConsistency_ObjectMetaMustBeSetOnError(t *testing.T) {
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
invalidV0 := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
Name: "test-dashboard",
UID: "test-uid",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0,
},
},
}
t.Run("Convert_V0_to_V1beta1 must set ObjectMeta on error", func(t *testing.T) {
target := &dashv1.Dashboard{}
_ = Convert_V0_to_V1beta1(invalidV0, target, nil)
require.Equal(t, invalidV0.Name, target.Name, "Name must be set on error")
require.Equal(t, invalidV0.Namespace, target.Namespace, "Namespace must be set on error")
require.Equal(t, dashv1.APIVERSION, target.APIVersion, "APIVersion must be set on error")
})
t.Run("Convert_V0_to_V2alpha1 must set ObjectMeta on error", func(t *testing.T) {
target := &dashv2alpha1.Dashboard{}
_ = Convert_V0_to_V2alpha1(invalidV0, target, nil, dsProvider, leProvider)
require.Equal(t, invalidV0.Name, target.Name, "Name must be set on error")
require.Equal(t, invalidV0.Namespace, target.Namespace, "Namespace must be set on error")
require.Equal(t, dashv2alpha1.APIVERSION, target.APIVersion, "APIVersion must be set on error")
})
t.Run("Convert_V0_to_V2beta1 must set ObjectMeta on error", func(t *testing.T) {
target := &dashv2beta1.Dashboard{}
_ = Convert_V0_to_V2beta1(invalidV0, target, nil, dsProvider, leProvider)
require.Equal(t, invalidV0.Name, target.Name, "Name must be set on error")
require.Equal(t, invalidV0.Namespace, target.Namespace, "Namespace must be set on error")
require.Equal(t, dashv2beta1.APIVERSION, target.APIVersion, "APIVersion must be set on error")
})
}
// TestV0ConversionConsistency_LayoutMustBeSetOnError ensures v2alpha1 and v2beta1 targets
// have a default layout set even when conversion fails to prevent JSON marshaling errors.
func TestV0ConversionConsistency_LayoutMustBeSetOnError(t *testing.T) {
dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig)
leProvider := migrationtestutil.NewLibraryElementProvider()
migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL)
invalidV0 := &dashv0.Dashboard{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-1",
Name: "test-dashboard",
},
Spec: common.Unstructured{
Object: map[string]interface{}{
"title": "test dashboard",
"schemaVersion": 0,
},
},
}
t.Run("Convert_V0_to_V2alpha1 must set default layout on error", func(t *testing.T) {
target := &dashv2alpha1.Dashboard{}
_ = Convert_V0_to_V2alpha1(invalidV0, target, nil, dsProvider, leProvider)
require.NotNil(t, target.Spec.Layout.GridLayoutKind, "GridLayoutKind must be set on error to prevent JSON marshaling issues")
})
t.Run("Convert_V0_to_V2beta1 must set default layout on error", func(t *testing.T) {
target := &dashv2beta1.Dashboard{}
_ = Convert_V0_to_V2beta1(invalidV0, target, nil, dsProvider, leProvider)
require.NotNil(t, target.Spec.Layout.GridLayoutKind, "GridLayoutKind must be set on error to prevent JSON marshaling issues")
})
}