Dashboards: Add Dashboard Schema validation (2) (#103844)

* Activate schema validation and align underlying systems

* update to save as v0 if not the right schema version

* Resolve merge conflicts

* Move RequireApiErrorStatus to tests package

* Add mutation tests

* Fix lint

* Only do min version check if dashboard is v1

* Fix lint and disable provisioning test

* Revert provisioning changes

* Revert more tests and add schema test

* Reran gen

* SQL Dashboard save

* Adjust APIVERSION

* Fixed mutation test

* Add logging on downgrade

---------

Co-authored-by: Marco de Abreu <18629099+marcoabreu@users.noreply.github.com>
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
This commit is contained in:
Marco de Abreu
2025-04-11 23:05:41 +02:00
committed by GitHub
co-authored by Marco de Abreu Stephanie Hingtgen
parent 07a225649d
commit c47ab101d1
21 changed files with 500 additions and 128 deletions
@@ -7,7 +7,9 @@ import (
"strings"
"testing"
dashboardv0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
folders "github.com/grafana/grafana/pkg/apis/folder/v1"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -219,19 +221,88 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
t.Run("Dashboard schema validations", func(t *testing.T) {
// Test invalid dashboard schema
t.Run("reject dashboard with invalid schema", func(t *testing.T) {
dashObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(),
"kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind,
"metadata": map[string]interface{}{
"generateName": "test-",
testCases := []struct {
name string
resourceInfo utils.ResourceInfo
expectSpecErr bool
testObject *unstructured.Unstructured
}{
{
name: "v0alpha1 dashboard with wrong spec should not throw on v0",
resourceInfo: dashboardv0alpha1.DashboardResourceInfo,
expectSpecErr: false,
testObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardv0alpha1.DashboardResourceInfo.TypeMeta().APIVersion,
"kind": "Dashboard",
"metadata": map[string]interface{}{
"generateName": "test-",
},
"spec": map[string]interface{}{
"title": "Dashboard Title",
"schemaVersion": 41,
"editable": "elephant",
"time": 9000,
"uid": strings.Repeat("a", 100),
},
},
},
},
{
name: "v1 dashboard with wrong spec should throw on v1",
resourceInfo: dashboardv1.DashboardResourceInfo,
expectSpecErr: true,
testObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardv1.DashboardResourceInfo.TypeMeta().APIVersion,
"kind": "Dashboard",
"metadata": map[string]interface{}{
"generateName": "test-",
},
"spec": map[string]interface{}{
"title": "Dashboard Title",
"schemaVersion": 41,
"editable": "elephant",
"time": 9000,
"uid": strings.Repeat("a", 100),
},
},
},
},
{
name: "v2alpha1 dashboard with correct spec should not throw on v2",
resourceInfo: dashboardv2alpha1.DashboardResourceInfo,
expectSpecErr: false,
testObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardv2alpha1.DashboardResourceInfo.TypeMeta().APIVersion,
"kind": "Dashboard",
"metadata": map[string]interface{}{
"generateName": "test-",
},
"spec": map[string]interface{}{
"title": "Dashboard Title",
"description": "valid description",
},
},
},
// Missing spec
},
}
_, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.Error(t, err)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
resourceClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, tc.resourceInfo.GroupVersionResource())
createdDashboard, err := resourceClient.Resource.Create(context.Background(), tc.testObject, v1.CreateOptions{})
if tc.expectSpecErr {
ctx.Helper.RequireApiErrorStatus(err, v1.StatusReasonInvalid, http.StatusUnprocessableEntity)
} else {
require.NoError(t, err)
require.NotNil(t, createdDashboard)
err = resourceClient.Resource.Delete(context.Background(), createdDashboard.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
}
})
}
})
})
@@ -683,20 +754,12 @@ func createTestContext(t *testing.T, helper *apis.K8sTestHelper, orgUsers apis.O
// getDashboardGVR returns the dashboard GroupVersionResource
func getDashboardGVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: dashboardv1.DashboardResourceInfo.GroupVersion().Group,
Version: dashboardv1.DashboardResourceInfo.GroupVersion().Version,
Resource: dashboardv1.DashboardResourceInfo.GetName(),
}
return dashboardv1.DashboardResourceInfo.GroupVersionResource()
}
// getFolderGVR returns the folder GroupVersionResource
func getFolderGVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: folders.FolderResourceInfo.GroupVersion().Group,
Version: folders.FolderResourceInfo.GroupVersion().Version,
Resource: folders.FolderResourceInfo.GetName(),
}
return folders.FolderResourceInfo.GroupVersionResource()
}
// Get a resource client for the specified user