Dashboards API: v2alpha2 missing pieces (#108293)

* Dashboards API: v2alpha2 missing pieces

* Fix issue with dashboard client scope for alpha versions

As we now have 2 different alpha versions for v2 we need to store the
clients separately.

* Improve debuggability of provisioning export test

- Add a helper function to print the tree structure.
- Be explicit about the expected file names expected in each case.

* Update pkg/registry/apis/dashboard/mutate.go

* Update pkg/services/authz/zanzana/server/server.go

Co-authored-by: Igor Suleymanov <radiohead@users.noreply.github.com>

* Review

* go lint

---------

Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com>
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
Co-authored-by: Igor Suleymanov <radiohead@users.noreply.github.com>
This commit is contained in:
Dominik Prokop
2025-07-22 11:44:05 +02:00
committed by GitHub
co-authored by Igor Suleymanov Roberto Jimenez Sanchez Stephanie Hingtgen
parent eeb44c1d63
commit d72a70d246
15 changed files with 877 additions and 512 deletions
+33 -6
View File
@@ -23,7 +23,8 @@ import (
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
)
func TestMain(m *testing.M) {
@@ -164,10 +165,10 @@ func TestIntegrationDashboardsAppV1(t *testing.T) {
}
}
func TestIntegrationDashboardsAppV2(t *testing.T) {
func TestIntegrationDashboardsAppV2alpha1(t *testing.T) {
gvr := schema.GroupVersionResource{
Group: dashboardV2.GROUP,
Version: dashboardV2.VERSION,
Group: dashboardV2alpha1.GROUP,
Version: dashboardV2alpha1.VERSION,
Resource: "dashboards",
}
if testing.Short() {
@@ -176,7 +177,33 @@ func TestIntegrationDashboardsAppV2(t *testing.T) {
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
for _, mode := range modes {
t.Run(fmt.Sprintf("v1beta1 with dual writer mode %d", mode), func(t *testing.T) {
t.Run(fmt.Sprintf("v2alpha1 with dual writer mode %d", mode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {
DualWriterMode: mode,
},
},
})
runDashboardTest(t, helper, gvr)
})
}
}
func TestIntegrationDashboardsAppV2alpha2(t *testing.T) {
gvr := schema.GroupVersionResource{
Group: dashboardV2alpha2.GROUP,
Version: dashboardV2alpha2.VERSION,
Resource: "dashboards",
}
if testing.Short() {
t.Skip("skipping integration test")
}
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
for _, mode := range modes {
t.Run(fmt.Sprintf("v1alpha2 with dual writer mode %d", mode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
@@ -223,7 +250,7 @@ func TestIntegrationLegacySupport(t *testing.T) {
clientV2 := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
GVR: dashboardV2.DashboardResourceInfo.GroupVersionResource(),
GVR: dashboardV2alpha1.DashboardResourceInfo.GroupVersionResource(),
})
obj, err = clientV2.Resource.Create(ctx,
helper.LoadYAMLOrJSONFile("testdata/dashboard-test-v2.yaml"),
@@ -16,7 +16,8 @@ import (
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -345,11 +346,29 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
},
{
name: "v2alpha1 dashboard with correct spec should not throw on v2",
resourceInfo: dashboardV2.DashboardResourceInfo,
resourceInfo: dashboardV2alpha1.DashboardResourceInfo,
expectSpecErr: false,
testObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardV2.DashboardResourceInfo.TypeMeta().APIVersion,
"apiVersion": dashboardV2alpha1.DashboardResourceInfo.TypeMeta().APIVersion,
"kind": "Dashboard",
"metadata": map[string]interface{}{
"generateName": "test-",
},
"spec": map[string]interface{}{
"title": "Dashboard Title",
"description": "valid description",
},
},
},
},
{
name: "v2alpha2 dashboard with correct spec should not throw on v2",
resourceInfo: dashboardV2alpha2.DashboardResourceInfo,
expectSpecErr: false,
testObject: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardV2alpha2.DashboardResourceInfo.TypeMeta().APIVersion,
"kind": "Dashboard",
"metadata": map[string]interface{}{
"generateName": "test-",
@@ -1,9 +1,9 @@
apiVersion: dashboard.grafana.app/v2alpha1
kind: Dashboard
metadata:
name: test-v2
name: test-v2alpha1
spec:
title: Test dashboard. Created at v2
title: Test dashboard. Created at v2alpha1
layout:
kind: GridLayout
spec:
@@ -0,0 +1,10 @@
apiVersion: dashboard.grafana.app/v2alpha2
kind: Dashboard
metadata:
name: test-v2alpha2
spec:
title: Test dashboard. Created at v2alpha2
layout:
kind: GridLayout
spec:
items: []
+29 -21
View File
@@ -21,7 +21,8 @@ import (
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashboardsV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
dashboardsV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
folder "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
@@ -41,15 +42,16 @@ type provisioningTestHelper struct {
*apis.K8sTestHelper
ProvisioningPath string
Repositories *apis.K8sResourceClient
Jobs *apis.K8sResourceClient
Folders *apis.K8sResourceClient
DashboardsV0 *apis.K8sResourceClient
DashboardsV1 *apis.K8sResourceClient
DashboardsV2 *apis.K8sResourceClient
AdminREST *rest.RESTClient
EditorREST *rest.RESTClient
ViewerREST *rest.RESTClient
Repositories *apis.K8sResourceClient
Jobs *apis.K8sResourceClient
Folders *apis.K8sResourceClient
DashboardsV0 *apis.K8sResourceClient
DashboardsV1 *apis.K8sResourceClient
DashboardsV2alpha1 *apis.K8sResourceClient
DashboardsV2alpha2 *apis.K8sResourceClient
AdminREST *rest.RESTClient
EditorREST *rest.RESTClient
ViewerREST *rest.RESTClient
}
func (h *provisioningTestHelper) SyncAndWait(t *testing.T, repo string, options *provisioning.SyncJobOptions) {
@@ -255,10 +257,15 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper
Namespace: "default", // actually org1
GVR: dashboardV1.DashboardResourceInfo.GroupVersionResource(),
})
dashboardsV2 := helper.GetResourceClient(apis.ResourceClientArgs{
dashboardsV2alpha1 := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: "default", // actually org1
GVR: dashboardV2.DashboardResourceInfo.GroupVersionResource(),
GVR: dashboardsV2alpha1.DashboardResourceInfo.GroupVersionResource(),
})
dashboardsV2alpha2 := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: "default", // actually org1
GVR: dashboardsV2alpha2.DashboardResourceInfo.GroupVersionResource(),
})
// Repo client, but less guard rails. Useful for subresources. We'll need this later...
@@ -289,15 +296,16 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper
ProvisioningPath: provisioningPath,
K8sTestHelper: helper,
Repositories: repositories,
AdminREST: adminClient,
EditorREST: editorClient,
ViewerREST: viewerClient,
Jobs: jobs,
Folders: folders,
DashboardsV0: dashboardsV0,
DashboardsV1: dashboardsV1,
DashboardsV2: dashboardsV2,
Repositories: repositories,
AdminREST: adminClient,
EditorREST: editorClient,
ViewerREST: viewerClient,
Jobs: jobs,
Folders: folders,
DashboardsV0: dashboardsV0,
DashboardsV1: dashboardsV1,
DashboardsV2alpha1: dashboardsV2alpha1,
DashboardsV2alpha2: dashboardsV2alpha2,
}
}
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
@@ -21,11 +22,50 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/tests/apis"
)
// printFileTree prints the directory structure as a tree for debugging purposes
func printFileTree(t *testing.T, rootPath string) {
t.Helper()
t.Logf("File tree for %s:", rootPath)
err := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(rootPath, path)
if err != nil {
return err
}
if relPath == "." {
return nil
}
depth := strings.Count(relPath, string(filepath.Separator))
indent := strings.Repeat(" ", depth)
if d.IsDir() {
t.Logf("%s├── %s/", indent, d.Name())
} else {
info, err := d.Info()
if err != nil {
t.Logf("%s├── %s (error reading info)", indent, d.Name())
} else {
t.Logf("%s├── %s (%d bytes)", indent, d.Name(), info.Size())
}
}
return nil
})
if err != nil {
t.Logf("Error walking directory: %v", err)
}
}
func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -609,9 +649,13 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) {
_, err = helper.DashboardsV1.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create v1 dashboard")
dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2.yaml")
_, err = helper.DashboardsV2.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create v2 dashboard")
dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2alpha1.yaml")
_, err = helper.DashboardsV2alpha1.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create v2alpha1 dashboard")
dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2alpha2.yaml")
_, err = helper.DashboardsV2alpha2.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create v2alpha2 dashboard")
// Now for the repository.
const repo = "local-repository"
@@ -642,15 +686,19 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) {
title string
apiVersion string
name string
fileName string
}
printFileTree(t, helper.ProvisioningPath)
// Check that each file was exported with its stored version
for _, test := range []props{
{title: "Test dashboard. Created at v0", apiVersion: "dashboard.grafana.app/v0alpha1", name: "test-v0"},
{title: "Test dashboard. Created at v1", apiVersion: "dashboard.grafana.app/v1beta1", name: "test-v1"},
{title: "Test dashboard. Created at v2", apiVersion: "dashboard.grafana.app/v2alpha1", name: "test-v2"},
{title: "Test dashboard. Created at v0", apiVersion: "dashboard.grafana.app/v0alpha1", name: "test-v0", fileName: "test-dashboard-created-at-v0.json"},
{title: "Test dashboard. Created at v1", apiVersion: "dashboard.grafana.app/v1beta1", name: "test-v1", fileName: "test-dashboard-created-at-v1.json"},
{title: "Test dashboard. Created at v2alpha1", apiVersion: "dashboard.grafana.app/v2alpha1", name: "test-v2alpha1", fileName: "test-dashboard-created-at-v2alpha1.json"},
{title: "Test dashboard. Created at v2alpha2", apiVersion: "dashboard.grafana.app/v2alpha2", name: "test-v2alpha2", fileName: "test-dashboard-created-at-v2alpha2.json"},
} {
fpath := filepath.Join(helper.ProvisioningPath, slugify.Slugify(test.title)+".json")
fpath := filepath.Join(helper.ProvisioningPath, test.fileName)
//nolint:gosec // we are ok with reading files in testdata
body, err := os.ReadFile(fpath)
require.NoError(t, err, "exported file was not created at path %s", fpath)