package dashboard import ( "encoding/json" "os" "testing" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" 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" ) func TestLargeDashboardSupportV1(t *testing.T) { devdash := "../../../../devenv/dev-dashboards/all-panels.json" // nolint:gosec // We can ignore the gosec G304 warning because this is a test with hardcoded input values f, err := os.ReadFile(devdash) require.NoError(t, err) dash := &dashv1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "test", }, } err = json.Unmarshal(f, &dash.Spec) require.NoError(t, err) expectedPanelCount := 19 panels, found, err := unstructured.NestedSlice(dash.Spec.Object, "panels") require.NoError(t, err) require.True(t, found) require.Len(t, panels, expectedPanelCount) scheme := runtime.NewScheme() err = dashv1.AddToScheme(scheme) require.NoError(t, err) largeObject := NewDashboardLargeObjectSupport(scheme, 0) // Convert the dashboard to a small value err = largeObject.ReduceSpec(dash) require.NoError(t, err) small, err := json.MarshalIndent(&dash.Spec, "", " ") require.NoError(t, err) require.JSONEq(t, `{ "schemaVersion": 36, "title": "Panel tests - All panels", "tags": ["gdev","panel-tests","all-panels"] }`, string(small)) // Now make it big again rehydratedDash := &dashv1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "test", }, } err = largeObject.RebuildSpec(rehydratedDash, f) require.NoError(t, err) // check that all panels exist again panels, found, err = unstructured.NestedSlice(rehydratedDash.Spec.Object, "panels") require.NoError(t, err) require.True(t, found) require.Len(t, panels, expectedPanelCount) } func TestLargeDashboardSupportV2alpha1(t *testing.T) { // Test RebuildSpec functionality specifically for v2 dashboards // This tests the json.Unmarshal(blob, &dash.Spec) path for structured specs // unlike v0/v1 which use the UnmarshalJSON path for unstructured specs // Create a v2 dashboard with structured spec originalV2Dash := &dashv2alpha1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test-v2", Namespace: "test", }, Spec: dashv2alpha1.DashboardSpec{ Title: "Test V2 Dashboard", Description: stringPtr("A test dashboard for v2 large object support"), Tags: []string{"test", "v2", "large-object"}, Editable: boolPtr(true), LiveNow: boolPtr(false), Preload: false, Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{ { Kind: "AnnotationQuery", Spec: dashv2alpha1.DashboardAnnotationQuerySpec{ Name: "Test Annotation", }, }, }, Elements: map[string]dashv2alpha1.DashboardElement{ "panel-1": { PanelKind: &dashv2alpha1.DashboardPanelKind{}, }, }, Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{}, TimeSettings: dashv2alpha1.DashboardTimeSettingsSpec{}, CursorSync: dashv2alpha1.DashboardDashboardCursorSyncOff, Variables: []dashv2alpha1.DashboardVariableKind{}, Links: []dashv2alpha1.DashboardDashboardLink{}, }, } scheme := runtime.NewScheme() err := dashv2alpha1.AddToScheme(scheme) require.NoError(t, err) largeObject := NewDashboardLargeObjectSupport(scheme, 0) // Marshal the original spec to use as our "blob" data originalSpecBlob, err := json.Marshal(originalV2Dash.Spec) require.NoError(t, err) // Create a copy to test reduction dashToReduce := originalV2Dash.DeepCopy() // Convert the dashboard to a small value (ReduceSpec) err = largeObject.ReduceSpec(dashToReduce) require.NoError(t, err) // Verify only essential fields remain after reduction require.Equal(t, "Test V2 Dashboard", dashToReduce.Spec.Title) require.Equal(t, stringPtr("A test dashboard for v2 large object support"), dashToReduce.Spec.Description) require.Equal(t, []string{"test", "v2", "large-object"}, dashToReduce.Spec.Tags) // Everything else should be empty/default require.Empty(t, dashToReduce.Spec.Annotations) require.Empty(t, dashToReduce.Spec.Elements) require.Nil(t, dashToReduce.Spec.Layout.GridLayoutKind) require.Empty(t, dashToReduce.Spec.Variables) require.Empty(t, dashToReduce.Spec.Links) // Now test RebuildSpec - this is the key test for v2! rehydratedDash := &dashv2alpha1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test-v2-rehydrated", Namespace: "test", }, } // This tests the json.Unmarshal(blob, &dash.Spec) path for v2 dashboards err = largeObject.RebuildSpec(rehydratedDash, originalSpecBlob) require.NoError(t, err) // Verify the full dashboard spec is restored correctly require.Equal(t, originalV2Dash.Spec.Title, rehydratedDash.Spec.Title) require.Equal(t, originalV2Dash.Spec.Description, rehydratedDash.Spec.Description) require.Equal(t, originalV2Dash.Spec.Tags, rehydratedDash.Spec.Tags) require.Equal(t, originalV2Dash.Spec.Editable, rehydratedDash.Spec.Editable) require.Equal(t, originalV2Dash.Spec.LiveNow, rehydratedDash.Spec.LiveNow) require.Equal(t, originalV2Dash.Spec.Preload, rehydratedDash.Spec.Preload) // Verify annotations are restored require.Len(t, rehydratedDash.Spec.Annotations, 1) annotation := rehydratedDash.Spec.Annotations[0] require.Equal(t, "AnnotationQuery", annotation.Kind) require.Equal(t, "Test Annotation", annotation.Spec.Name) // Verify elements are restored require.Len(t, rehydratedDash.Spec.Elements, 1) _, exists := rehydratedDash.Spec.Elements["panel-1"] require.True(t, exists) } func TestLargeDashboardSupportV2beta1(t *testing.T) { // Test RebuildSpec functionality specifically for v2 dashboards // This tests the json.Unmarshal(blob, &dash.Spec) path for structured specs // unlike v0/v1 which use the UnmarshalJSON path for unstructured specs // Create a v2 dashboard with structured spec originalV2Dash := &dashv2beta1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test-v2", Namespace: "test", }, Spec: dashv2beta1.DashboardSpec{ Title: "Test V2 Dashboard", Description: stringPtr("A test dashboard for v2 large object support"), Tags: []string{"test", "v2", "large-object"}, Editable: boolPtr(true), LiveNow: boolPtr(false), Preload: false, Annotations: []dashv2beta1.DashboardAnnotationQueryKind{ { Kind: "AnnotationQuery", Spec: dashv2beta1.DashboardAnnotationQuerySpec{ Name: "Test Annotation", }, }, }, Elements: map[string]dashv2beta1.DashboardElement{ "panel-1": { PanelKind: &dashv2beta1.DashboardPanelKind{}, }, }, Layout: dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{}, TimeSettings: dashv2beta1.DashboardTimeSettingsSpec{}, CursorSync: dashv2beta1.DashboardDashboardCursorSyncOff, Variables: []dashv2beta1.DashboardVariableKind{}, Links: []dashv2beta1.DashboardDashboardLink{}, }, } scheme := runtime.NewScheme() err := dashv2beta1.AddToScheme(scheme) require.NoError(t, err) largeObject := NewDashboardLargeObjectSupport(scheme, 0) // Marshal the original spec to use as our "blob" data originalSpecBlob, err := json.Marshal(originalV2Dash.Spec) require.NoError(t, err) // Create a copy to test reduction dashToReduce := originalV2Dash.DeepCopy() // Convert the dashboard to a small value (ReduceSpec) err = largeObject.ReduceSpec(dashToReduce) require.NoError(t, err) // Verify only essential fields remain after reduction require.Equal(t, "Test V2 Dashboard", dashToReduce.Spec.Title) require.Equal(t, stringPtr("A test dashboard for v2 large object support"), dashToReduce.Spec.Description) require.Equal(t, []string{"test", "v2", "large-object"}, dashToReduce.Spec.Tags) // Everything else should be empty/default require.Empty(t, dashToReduce.Spec.Annotations) require.Empty(t, dashToReduce.Spec.Elements) require.Nil(t, dashToReduce.Spec.Layout.GridLayoutKind) require.Empty(t, dashToReduce.Spec.Variables) require.Empty(t, dashToReduce.Spec.Links) // Now test RebuildSpec - this is the key test for v2! rehydratedDash := &dashv2beta1.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: "test-v2-rehydrated", Namespace: "test", }, } // This tests the json.Unmarshal(blob, &dash.Spec) path for v2 dashboards err = largeObject.RebuildSpec(rehydratedDash, originalSpecBlob) require.NoError(t, err) // Verify the full dashboard spec is restored correctly require.Equal(t, originalV2Dash.Spec.Title, rehydratedDash.Spec.Title) require.Equal(t, originalV2Dash.Spec.Description, rehydratedDash.Spec.Description) require.Equal(t, originalV2Dash.Spec.Tags, rehydratedDash.Spec.Tags) require.Equal(t, originalV2Dash.Spec.Editable, rehydratedDash.Spec.Editable) require.Equal(t, originalV2Dash.Spec.LiveNow, rehydratedDash.Spec.LiveNow) require.Equal(t, originalV2Dash.Spec.Preload, rehydratedDash.Spec.Preload) // Verify annotations are restored require.Len(t, rehydratedDash.Spec.Annotations, 1) annotation := rehydratedDash.Spec.Annotations[0] require.Equal(t, "AnnotationQuery", annotation.Kind) require.Equal(t, "Test Annotation", annotation.Spec.Name) // Verify elements are restored require.Len(t, rehydratedDash.Spec.Elements, 1) _, exists := rehydratedDash.Spec.Elements["panel-1"] require.True(t, exists) } // Helper functions for pointer types func stringPtr(s string) *string { return &s } func boolPtr(b bool) *bool { return &b }