test(provisioning): add integration tests for bulk export with Resources field

- Test exporting specific unmanaged dashboards
- Test exporting with custom path
- Test validation rejects folders
- Test validation rejects managed resources
- Test folder structure preservation
- Test empty resources list validation
This commit is contained in:
Roberto Jimenez Sanchez
2025-12-02 18:17:54 +01:00
parent ad8fb1005d
commit b1b105f667
@@ -0,0 +1,409 @@
package provisioning
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestIntegrationProvisioning_ExportSpecificResources(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
// Create unmanaged dashboards directly in Grafana
dashboard1 := helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v1.yaml")
dashboard1Obj, err := helper.DashboardsV1.Resource.Create(ctx, dashboard1, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create first dashboard")
dashboard1Name := dashboard1Obj.GetName()
dashboard2 := helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2beta1.yaml")
dashboard2Obj, err := helper.DashboardsV2beta1.Resource.Create(ctx, dashboard2, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create second dashboard")
dashboard2Name := dashboard2Obj.GetName()
// Verify dashboards are unmanaged
dash1, err := helper.DashboardsV1.Resource.Get(ctx, dashboard1Name, metav1.GetOptions{})
require.NoError(t, err)
manager1, found1 := dash1.GetAnnotations()[utils.AnnoKeyManagerIdentity]
require.True(t, !found1 || manager1 == "", "dashboard1 should be unmanaged")
dash2, err := helper.DashboardsV2beta1.Resource.Get(ctx, dashboard2Name, metav1.GetOptions{})
require.NoError(t, err)
manager2, found2 := dash2.GetAnnotations()[utils.AnnoKeyManagerIdentity]
require.True(t, !found2 || manager2 == "", "dashboard2 should be unmanaged")
// Create repository
const repo = "export-resources-test-repo"
testRepo := TestRepo{
Name: repo,
Copies: map[string]string{},
ExpectedDashboards: 0, // No dashboards expected after sync (we'll export manually)
ExpectedFolders: 0,
}
helper.CreateRepo(t, testRepo)
// Export specific dashboards using Resources field
spec := provisioning.JobSpec{
Action: provisioning.JobActionPush,
Push: &provisioning.ExportJobOptions{
Path: "",
Resources: []provisioning.ResourceRef{
{
Name: dashboard1Name,
Kind: "Dashboard",
Group: "dashboard.grafana.app",
},
{
Name: dashboard2Name,
Kind: "Dashboard",
Group: "dashboard.grafana.app",
},
},
},
}
helper.TriggerJobAndWaitForSuccess(t, repo, spec)
// Verify both dashboards were exported
dashboard1File := filepath.Join(helper.ProvisioningPath, "test-dashboard-created-at-v1.json")
dashboard2File := filepath.Join(helper.ProvisioningPath, "test-dashboard-created-at-v2beta1.json")
// Check dashboard1
body1, err := os.ReadFile(dashboard1File)
require.NoError(t, err, "exported file should exist for dashboard1")
obj1 := map[string]any{}
err = json.Unmarshal(body1, &obj1)
require.NoError(t, err, "exported file should be valid JSON")
val, _, err := unstructured.NestedString(obj1, "metadata", "name")
require.NoError(t, err)
require.Equal(t, "test-v1", val)
// Check dashboard2
body2, err := os.ReadFile(dashboard2File)
require.NoError(t, err, "exported file should exist for dashboard2")
obj2 := map[string]any{}
err = json.Unmarshal(body2, &obj2)
require.NoError(t, err, "exported file should be valid JSON")
val, _, err = unstructured.NestedString(obj2, "metadata", "name")
require.NoError(t, err)
require.Equal(t, "test-v2beta1", val)
}
func TestIntegrationProvisioning_ExportSpecificResourcesWithPath(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
// Create unmanaged dashboard
dashboard := helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v1.yaml")
dashboardObj, err := helper.DashboardsV1.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create dashboard")
dashboardName := dashboardObj.GetName()
// Create repository
const repo = "export-resources-path-test-repo"
testRepo := TestRepo{
Name: repo,
Copies: map[string]string{},
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, testRepo)
// Export with custom path
spec := provisioning.JobSpec{
Action: provisioning.JobActionPush,
Push: &provisioning.ExportJobOptions{
Path: "custom/path",
Resources: []provisioning.ResourceRef{
{
Name: dashboardName,
Kind: "Dashboard",
Group: "dashboard.grafana.app",
},
},
},
}
helper.TriggerJobAndWaitForSuccess(t, repo, spec)
// Verify dashboard was exported to custom path
expectedFile := filepath.Join(helper.ProvisioningPath, "custom", "path", "test-dashboard-created-at-v1.json")
body, err := os.ReadFile(expectedFile)
require.NoError(t, err, "exported file should exist at custom path")
obj := map[string]any{}
err = json.Unmarshal(body, &obj)
require.NoError(t, err, "exported file should be valid JSON")
val, _, err := unstructured.NestedString(obj, "metadata", "name")
require.NoError(t, err)
require.Equal(t, "test-v1", val)
}
func TestIntegrationProvisioning_ExportSpecificResourcesRejectsFolders(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
// Create a folder
folder := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "folder.grafana.app/v1beta1",
"kind": "Folder",
"metadata": map[string]any{
"name": "test-folder",
},
"spec": map[string]any{
"title": "Test Folder",
},
},
}
folderObj, err := helper.Folders.Resource.Create(ctx, folder, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create folder")
folderName := folderObj.GetName()
// Create repository
const repo = "export-reject-folders-test-repo"
testRepo := TestRepo{
Name: repo,
Copies: map[string]string{},
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, testRepo)
// Try to export folder (should fail validation)
spec := provisioning.JobSpec{
Action: provisioning.JobActionPush,
Push: &provisioning.ExportJobOptions{
Resources: []provisioning.ResourceRef{
{
Name: folderName,
Kind: "Folder",
Group: "folder.grafana.app",
},
},
},
}
// This should fail with validation error
body := asJSON(spec)
result := helper.AdminREST.Post().
Namespace("default").
Resource("repositories").
Name(repo).
SubResource("jobs").
Body(body).
SetHeader("Content-Type", "application/json").
Do(ctx)
err = result.Error()
require.Error(t, err, "should fail validation when trying to export folder")
require.Contains(t, err.Error(), "folders are not supported", "error should mention folders are not supported")
}
func TestIntegrationProvisioning_ExportSpecificResourcesRejectsManagedResources(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
// Create a managed dashboard via repository sync
testRepo := TestRepo{
Name: "managed-dashboard-repo",
Copies: map[string]string{
"exportunifiedtorepository/dashboard-test-v1.yaml": "dashboard.json",
},
ExpectedDashboards: 1,
ExpectedFolders: 0,
}
helper.CreateRepo(t, testRepo)
// Get the managed dashboard
dashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Len(t, dashboards.Items, 1, "should have one managed dashboard")
managedDashboard := dashboards.Items[0]
managedDashboardName := managedDashboard.GetName()
// Verify it's managed
manager, found := managedDashboard.GetAnnotations()[utils.AnnoKeyManagerIdentity]
require.True(t, found && manager != "", "dashboard should be managed")
// Create another repository for export
const exportRepo = "export-managed-reject-test-repo"
exportTestRepo := TestRepo{
Name: exportRepo,
Copies: map[string]string{},
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, exportTestRepo)
// Try to export managed dashboard (should fail)
spec := provisioning.JobSpec{
Action: provisioning.JobActionPush,
Push: &provisioning.ExportJobOptions{
Resources: []provisioning.ResourceRef{
{
Name: managedDashboardName,
Kind: "Dashboard",
Group: "dashboard.grafana.app",
},
},
},
}
// This should fail because the resource is managed
body := asJSON(spec)
result := helper.AdminREST.Post().
Namespace("default").
Resource("repositories").
Name(exportRepo).
SubResource("jobs").
Body(body).
SetHeader("Content-Type", "application/json").
Do(ctx)
// Wait for job to complete and check it failed
obj, err := result.Get()
require.NoError(t, err, "job should be created")
unstruct, ok := obj.(*unstructured.Unstructured)
require.True(t, ok, "should get unstructured object")
// Wait for job to complete
job := helper.AwaitJob(t, ctx, unstruct)
lastState := mustNestedString(job.Object, "status", "state")
lastErrors := mustNestedStringSlice(job.Object, "status", "errors")
// Job should fail with error about managed resource
require.Equal(t, string(provisioning.JobStateError), lastState, "job should fail")
require.NotEmpty(t, lastErrors, "job should have errors")
require.Contains(t, lastErrors[0], "managed", "error should mention managed resource")
}
func TestIntegrationProvisioning_ExportSpecificResourcesWithFolderStructure(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
// Create an unmanaged folder
folder := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "folder.grafana.app/v1beta1",
"kind": "Folder",
"metadata": map[string]any{
"name": "test-export-folder",
},
"spec": map[string]any{
"title": "Test Export Folder",
},
},
}
folderObj, err := helper.Folders.Resource.Create(ctx, folder, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create folder")
folderUID := folderObj.GetUID()
// Verify folder is unmanaged
manager, found := folderObj.GetAnnotations()[utils.AnnoKeyManagerIdentity]
require.True(t, !found || manager == "", "folder should be unmanaged")
// Create unmanaged dashboard in the folder
dashboard := helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v1.yaml")
// Set folder UID in dashboard spec
unstructured.SetNestedField(dashboard.Object, string(folderUID), "spec", "folder")
dashboardObj, err := helper.DashboardsV1.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
require.NoError(t, err, "should be able to create dashboard in folder")
dashboardName := dashboardObj.GetName()
// Create repository
const repo = "export-folder-structure-test-repo"
testRepo := TestRepo{
Name: repo,
Copies: map[string]string{},
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, testRepo)
// Export dashboard (should preserve folder structure)
spec := provisioning.JobSpec{
Action: provisioning.JobActionPush,
Push: &provisioning.ExportJobOptions{
Path: "",
Resources: []provisioning.ResourceRef{
{
Name: dashboardName,
Kind: "Dashboard",
Group: "dashboard.grafana.app",
},
},
},
}
helper.TriggerJobAndWaitForSuccess(t, repo, spec)
// Verify dashboard was exported with folder structure
// The folder path should be included in the file path based on the folder title
// Folder title is "Test Export Folder", which gets slugified to "test-export-folder"
expectedFile := filepath.Join(helper.ProvisioningPath, "test-export-folder", "test-dashboard-created-at-v1.json")
body, err := os.ReadFile(expectedFile)
require.NoError(t, err, "exported file should exist with folder structure")
obj := map[string]any{}
err = json.Unmarshal(body, &obj)
require.NoError(t, err, "exported file should be valid JSON")
val, _, err := unstructured.NestedString(obj, "metadata", "name")
require.NoError(t, err)
require.Equal(t, "test-v1", val)
}
func TestIntegrationProvisioning_ExportSpecificResourcesEmptyList(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
// Create repository
const repo = "export-empty-resources-test-repo"
testRepo := TestRepo{
Name: repo,
Copies: map[string]string{},
ExpectedDashboards: 0,
ExpectedFolders: 0,
}
helper.CreateRepo(t, testRepo)
// Try to export with empty resources list (should fail validation)
spec := provisioning.JobSpec{
Action: provisioning.JobActionPush,
Push: &provisioning.ExportJobOptions{
Resources: []provisioning.ResourceRef{},
},
}
// This should fail with validation error
body := asJSON(spec)
result := helper.AdminREST.Post().
Namespace("default").
Resource("repositories").
Name(repo).
SubResource("jobs").
Body(body).
SetHeader("Content-Type", "application/json").
Do(helper.Context())
err := result.Error()
require.Error(t, err, "should fail validation when resources list is empty")
}