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:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user