refactor: convert ExportSpecificResources tests to table-driven format

Converted all test cases in resources_specific_test.go to use a single
table-driven test function for better maintainability and consistency.

- Consolidated 10 separate test functions into one TestExportSpecificResources
- Each test case has clear structure: name, setupMocks, options, wantErr
- Makes it easier to add new test cases and maintain existing ones
- All tests passing with proper subtest naming

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Roberto Jimenez Sanchez
2025-12-03 12:40:15 +01:00
co-authored by Claude
parent 5d5dccc39c
commit 0f4f1dd8bf
@@ -17,545 +17,324 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
// Helper function to create folder objects
// name should be the Grafana folder UID (which becomes metadata.name)
// uid is the Kubernetes UID (metadata.uid)
// title defaults to name but can be overridden via spec.title
func createFolderObject(name, uid, parentFolderUID string) unstructured.Unstructured {
// createFolder creates a folder with the given Grafana UID as metadata.name and optional title
func createFolder(grafanaUID, k8sUID, title, parentUID string) unstructured.Unstructured {
folder := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.FolderResource.GroupVersion().String(),
"kind": "Folder",
"metadata": map[string]interface{}{
"name": name,
"uid": uid,
"name": grafanaUID, // Grafana UID is stored as metadata.name
"uid": k8sUID,
},
"spec": map[string]interface{}{
"title": name, // title defaults to name, tests can override if needed
"title": title,
},
},
}
if parentFolderUID != "" {
if parentUID != "" {
meta, _ := utils.MetaAccessor(&folder)
meta.SetFolder(parentFolderUID)
meta.SetFolder(parentUID)
}
return folder
}
// Helper function to create dashboard objects with folder
func createDashboardObjectWithFolder(name, folderID string) unstructured.Unstructured {
// createDashboardWithFolder creates a dashboard in the specified folder
func createDashboardWithFolder(name, folderUID string) unstructured.Unstructured {
dashboard := createDashboardObject(name)
if folderID != "" {
if folderUID != "" {
meta, _ := utils.MetaAccessor(&dashboard)
meta.SetFolder(folderID)
meta.SetFolder(folderUID)
}
return dashboard
}
// Helper function to run ExportSpecificResources test
func runExportSpecificResourcesTest(t *testing.T, resourceRefs []provisioningV0.ResourceRef, folderItems []unstructured.Unstructured, setupProgress func(*jobs.MockJobProgressRecorder), setupResources func(*resources.MockRepositoryResources, *resources.MockResourceClients)) error {
resourceClients := resources.NewMockResourceClients(t)
mockProgress := jobs.NewMockJobProgressRecorder(t)
setupProgress(mockProgress)
repoResources := resources.NewMockRepositoryResources(t)
setupResources(repoResources, resourceClients)
options := provisioningV0.ExportJobOptions{
Path: "grafana",
Branch: "feature/branch",
Resources: resourceRefs,
}
err := ExportSpecificResources(context.Background(), "test-repo", options, resourceClients, repoResources, mockProgress)
mockProgress.AssertExpectations(t)
repoResources.AssertExpectations(t)
resourceClients.AssertExpectations(t)
return err
}
func TestExportSpecificResources_Success(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
func TestExportSpecificResources(t *testing.T) {
tests := []struct {
name string
setupMocks func(t *testing.T) (resourceClients *resources.MockResourceClients, repoResources *resources.MockRepositoryResources, progress *jobs.MockJobProgressRecorder)
options provisioningV0.ExportJobOptions
wantErr string
assertResults func(t *testing.T, resourceClients *resources.MockResourceClients, repoResources *resources.MockRepositoryResources, progress *jobs.MockJobProgressRecorder)
}{
{
Name: "dashboard-1",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
},
{
Name: "dashboard-2",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
},
}
name: "success with folder paths",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
folder := createFolder("team-a-uid", "k8s-1", "team-a", "")
dashboard1 := createDashboardWithFolder("dashboard-1", "team-a-uid")
dashboard2 := createDashboardObject("dashboard-2")
// Create folder with UID as name (matching real Grafana behavior)
teamAFolder := createFolderObject("team-a-uid", "k8s-uid-1", "")
_ = unstructured.SetNestedField(teamAFolder.Object, "team-a", "spec", "title")
resourceClients := resources.NewMockResourceClients(t)
folderClient := &mockDynamicInterface{items: []unstructured.Unstructured{folder}}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
folderItems := []unstructured.Unstructured{teamAFolder}
gvk := schema.GroupVersionKind{Group: resources.DashboardResource.Group, Kind: "Dashboard"}
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{items: []unstructured.Unstructured{dashboard1}}, resources.DashboardResource, nil).Once()
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{items: []unstructured.Unstructured{dashboard2}}, resources.DashboardResource, nil).Once()
dashboard1 := createDashboardObjectWithFolder("dashboard-1", "team-a-uid")
dashboard2 := createDashboardObject("dashboard-2")
repoResources := resources.NewMockRepositoryResources(t)
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool { return obj.GetName() == "dashboard-1" }),
mock.MatchedBy(func(opts resources.WriteOptions) bool { return opts.Path == "grafana/team-a" })).
Return("grafana/team-a/dashboard-1.json", nil)
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool { return obj.GetName() == "dashboard-2" }),
mock.MatchedBy(func(opts resources.WriteOptions) bool { return opts.Path == "grafana" })).
Return("grafana/dashboard-2.json", nil)
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("SetMessage", mock.Anything, mock.AnythingOfType("string")).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "dashboard-1" && result.Action == repository.FileActionCreated
})).Return()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "dashboard-2" && result.Action == repository.FileActionCreated
})).Return()
progress.On("TooManyErrors").Return(nil).Times(2)
}
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "dashboard-1" && r.Action == repository.FileActionCreated
})).Return()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "dashboard-2" && r.Action == repository.FileActionCreated
})).Return()
progress.On("TooManyErrors").Return(nil).Times(2)
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
// Setup folder client
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
// Setup dashboard clients - need separate clients for Get calls
dashboard1Client := &mockDynamicInterface{
items: []unstructured.Unstructured{dashboard1},
}
dashboard2Client := &mockDynamicInterface{
items: []unstructured.Unstructured{dashboard2},
}
gvk := schema.GroupVersionKind{
Group: resources.DashboardResource.Group,
Kind: "Dashboard",
}
// First call returns dashboard1Client, second returns dashboard2Client
resourceClients.On("ForKind", mock.Anything, gvk).Return(dashboard1Client, resources.DashboardResource, nil).Once()
resourceClients.On("ForKind", mock.Anything, gvk).Return(dashboard2Client, resources.DashboardResource, nil).Once()
// Mock WriteResourceFileFromObject calls
// dashboard-1 is in team-a folder
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
return obj.GetName() == "dashboard-1"
}),
mock.MatchedBy(func(opts resources.WriteOptions) bool {
return opts.Path == "grafana/team-a" && opts.Ref == "feature/branch"
})).Return("grafana/team-a/dashboard-1.json", nil)
// dashboard-2 has no folder
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
return obj.GetName() == "dashboard-2"
}),
mock.MatchedBy(func(opts resources.WriteOptions) bool {
return opts.Path == "grafana" && opts.Ref == "feature/branch"
})).Return("grafana/dashboard-2.json", nil)
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.NoError(t, err)
}
func TestExportSpecificResources_EmptyResources(t *testing.T) {
options := provisioningV0.ExportJobOptions{
Path: "grafana",
Branch: "feature/branch",
Resources: []provisioningV0.ResourceRef{},
}
resourceClients := resources.NewMockResourceClients(t)
repoResources := resources.NewMockRepositoryResources(t)
mockProgress := jobs.NewMockJobProgressRecorder(t)
err := ExportSpecificResources(context.Background(), "test-repo", options, resourceClients, repoResources, mockProgress)
require.EqualError(t, err, "no resources specified for export")
}
func TestExportSpecificResources_RejectsFolders(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "my-folder",
Kind: "Folder",
Group: resources.FolderResource.Group,
},
}
folderItems := []unstructured.Unstructured{}
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "my-folder" &&
result.Action == repository.FileActionIgnored &&
result.Error != nil &&
result.Error.Error() == "folders are not supported for export"
})).Return()
progress.On("TooManyErrors").Return(nil)
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
// No ForKind or WriteResourceFileFromObject calls expected for folders
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.NoError(t, err)
}
func TestExportSpecificResources_RejectsManagedResources(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "managed-dashboard",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
},
}
folderItems := []unstructured.Unstructured{}
dashboard := createDashboardObject("managed-dashboard")
meta, _ := utils.MetaAccessor(&dashboard)
meta.SetManagerProperties(utils.ManagerProperties{
Kind: utils.ManagerKindRepo,
Identity: "some-repo",
})
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("SetMessage", mock.Anything, mock.AnythingOfType("string")).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "managed-dashboard" &&
result.Action == repository.FileActionIgnored &&
result.Error != nil &&
result.Error.Error() == "resource dashboard.grafana.app/Dashboard/managed-dashboard is managed and cannot be exported"
})).Return()
progress.On("TooManyErrors").Return(nil)
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
dashboardClient := &mockDynamicInterface{
items: []unstructured.Unstructured{dashboard},
}
gvk := schema.GroupVersionKind{
Group: resources.DashboardResource.Group,
Kind: "Dashboard",
}
resourceClients.On("ForKind", mock.Anything, gvk).Return(dashboardClient, resources.DashboardResource, nil)
// No WriteResourceFileFromObject call expected for managed resources
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.NoError(t, err)
}
func TestExportSpecificResources_RejectsUnsupportedResources(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "some-resource",
Kind: "Playlist",
Group: "playlist.grafana.app",
},
}
folderItems := []unstructured.Unstructured{}
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("SetMessage", mock.Anything, mock.AnythingOfType("string")).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "some-resource" &&
result.Action == repository.FileActionIgnored &&
result.Error != nil &&
result.Error.Error() == "resource type playlist.grafana.app/playlists is not supported for export"
})).Return()
progress.On("TooManyErrors").Return(nil)
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
// Mock ForKind to return a client (even though it's unsupported)
unsupportedClient := &mockDynamicInterface{items: []unstructured.Unstructured{}}
gvk := schema.GroupVersionKind{
Group: "playlist.grafana.app",
Kind: "Playlist",
}
gvr := schema.GroupVersionResource{
Group: "playlist.grafana.app",
Resource: "playlists",
}
resourceClients.On("ForKind", mock.Anything, gvk).Return(unsupportedClient, gvr, nil)
// No WriteResourceFileFromObject call expected for unsupported resources
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.NoError(t, err)
}
func TestExportSpecificResources_FolderPathResolution(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "dashboard-in-nested-folder",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
},
}
// Create folder hierarchy: team-a -> subteam
// In Grafana, folder UIDs are stored as metadata.name (matching real behavior)
parentFolder := createFolderObject("team-a-uid", "k8s-uid-1", "")
_ = unstructured.SetNestedField(parentFolder.Object, "team-a", "spec", "title")
childFolder := createFolderObject("subteam-uid", "k8s-uid-2", "team-a-uid")
_ = unstructured.SetNestedField(childFolder.Object, "subteam", "spec", "title")
folderItems := []unstructured.Unstructured{parentFolder, childFolder}
dashboard := createDashboardObjectWithFolder("dashboard-in-nested-folder", "subteam-uid")
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("SetMessage", mock.Anything, mock.AnythingOfType("string")).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "dashboard-in-nested-folder" && result.Action == repository.FileActionCreated
})).Return()
progress.On("TooManyErrors").Return(nil)
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
dashboardClient := &mockDynamicInterface{
items: []unstructured.Unstructured{dashboard},
}
gvk := schema.GroupVersionKind{
Group: resources.DashboardResource.Group,
Kind: "Dashboard",
}
resourceClients.On("ForKind", mock.Anything, gvk).Return(dashboardClient, resources.DashboardResource, nil)
// Verify that the path includes the nested folder structure: grafana/team-a/subteam
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
return obj.GetName() == "dashboard-in-nested-folder"
}),
mock.MatchedBy(func(opts resources.WriteOptions) bool {
return opts.Path == "grafana/team-a/subteam" && opts.Ref == "feature/branch"
})).Return("grafana/team-a/subteam/dashboard-in-nested-folder.json", nil)
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.NoError(t, err)
}
func TestExportSpecificResources_FolderClientError(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "dashboard-1",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
},
}
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
resourceClients.On("Folder", mock.Anything).Return(nil, fmt.Errorf("folder client error"))
}
err := runExportSpecificResourcesTest(t, resourceRefs, nil, setupProgress, setupResources)
require.EqualError(t, err, "get folder client: folder client error")
}
func TestExportSpecificResources_ResourceNotFound(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "non-existent-dashboard",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
},
}
folderItems := []unstructured.Unstructured{}
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("SetMessage", mock.Anything, mock.AnythingOfType("string")).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "non-existent-dashboard" &&
result.Error != nil &&
result.Error.Error() == "get resource dashboard.grafana.app/Dashboard/non-existent-dashboard: no items found"
})).Return()
progress.On("TooManyErrors").Return(nil)
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
// Empty client - resource not found
dashboardClient := &mockDynamicInterface{
items: []unstructured.Unstructured{},
}
gvk := schema.GroupVersionKind{
Group: resources.DashboardResource.Group,
Kind: "Dashboard",
}
resourceClients.On("ForKind", mock.Anything, gvk).Return(dashboardClient, resources.DashboardResource, nil)
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.NoError(t, err)
}
func TestExportSpecificResources_DashboardVersionConversion(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "v2-dashboard",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
},
}
folderItems := []unstructured.Unstructured{}
// Dashboard with storedVersion v2alpha1
v1Dashboard := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": "Dashboard",
"metadata": map[string]interface{}{
"name": "v2-dashboard",
return resourceClients, repoResources, progress
},
"status": map[string]interface{}{
"conversion": map[string]interface{}{
"failed": true,
"storedVersion": "v2alpha1",
options: provisioningV0.ExportJobOptions{
Path: "grafana",
Branch: "feature/branch",
Resources: []provisioningV0.ResourceRef{
{Name: "dashboard-1", Kind: "Dashboard", Group: resources.DashboardResource.Group},
{Name: "dashboard-2", Kind: "Dashboard", Group: resources.DashboardResource.Group},
},
},
},
}
v2Dashboard := createV2DashboardObject("v2-dashboard", "v2alpha1")
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("SetMessage", mock.Anything, mock.AnythingOfType("string")).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "v2-dashboard" && result.Action == repository.FileActionCreated
})).Return()
progress.On("TooManyErrors").Return(nil)
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
// v1 client returns dashboard with storedVersion
v1Client := &mockDynamicInterface{
items: []unstructured.Unstructured{v1Dashboard},
}
gvk := schema.GroupVersionKind{
Group: resources.DashboardResource.Group,
Kind: "Dashboard",
}
resourceClients.On("ForKind", mock.Anything, gvk).Return(v1Client, resources.DashboardResource, nil)
// v2alpha1 client for fetching original version
v2Client := &mockDynamicInterface{
items: []unstructured.Unstructured{v2Dashboard},
}
v2GVR := schema.GroupVersionResource{
Group: resources.DashboardResource.Group,
Version: "v2alpha1",
Resource: resources.DashboardResource.Resource,
}
resourceClients.On("ForResource", mock.Anything, v2GVR).Return(v2Client, gvk, nil)
// Verify WriteResourceFileFromObject is called with v2 dashboard
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
return obj.GetName() == "v2-dashboard" &&
obj.GetAPIVersion() == "dashboard.grafana.app/v2alpha1"
}),
mock.MatchedBy(func(opts resources.WriteOptions) bool {
return opts.Path == "grafana" && opts.Ref == "feature/branch"
})).Return("grafana/v2-dashboard.json", nil)
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.NoError(t, err)
}
func TestExportSpecificResources_TooManyErrors(t *testing.T) {
resourceRefs := []provisioningV0.ResourceRef{
{
Name: "dashboard-1",
Kind: "Dashboard",
Group: resources.DashboardResource.Group,
name: "empty resources returns error",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
return nil, nil, nil
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{},
},
wantErr: "no resources specified for export",
},
{
name: "rejects folders",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(&mockDynamicInterface{}, nil)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "my-folder" && r.Error != nil && r.Error.Error() == "folders are not supported for export"
})).Return()
progress.On("TooManyErrors").Return(nil)
return resourceClients, nil, progress
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{{Name: "my-folder", Kind: "Folder", Group: resources.FolderResource.Group}},
},
},
{
name: "rejects managed resources",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
dashboard := createDashboardObject("managed-dashboard")
meta, _ := utils.MetaAccessor(&dashboard)
meta.SetManagerProperties(utils.ManagerProperties{Kind: utils.ManagerKindRepo, Identity: "some-repo"})
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(&mockDynamicInterface{}, nil)
gvk := schema.GroupVersionKind{Group: resources.DashboardResource.Group, Kind: "Dashboard"}
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{items: []unstructured.Unstructured{dashboard}}, resources.DashboardResource, nil)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "managed-dashboard" && r.Error != nil && r.Error.Error() == "resource dashboard.grafana.app/Dashboard/managed-dashboard is managed and cannot be exported"
})).Return()
progress.On("TooManyErrors").Return(nil)
return resourceClients, nil, progress
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{{Name: "managed-dashboard", Kind: "Dashboard", Group: resources.DashboardResource.Group}},
},
},
{
name: "rejects unsupported resources",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(&mockDynamicInterface{}, nil)
gvk := schema.GroupVersionKind{Group: "playlist.grafana.app", Kind: "Playlist"}
gvr := schema.GroupVersionResource{Group: "playlist.grafana.app", Resource: "playlists"}
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{}, gvr, nil)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "some-resource" && r.Error != nil && r.Error.Error() == "resource type playlist.grafana.app/playlists is not supported for export"
})).Return()
progress.On("TooManyErrors").Return(nil)
return resourceClients, nil, progress
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{{Name: "some-resource", Kind: "Playlist", Group: "playlist.grafana.app"}},
},
},
{
name: "resolves nested folder paths",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
parentFolder := createFolder("team-a-uid", "k8s-1", "team-a", "")
childFolder := createFolder("subteam-uid", "k8s-2", "subteam", "team-a-uid")
dashboard := createDashboardWithFolder("dashboard-in-nested-folder", "subteam-uid")
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(&mockDynamicInterface{items: []unstructured.Unstructured{parentFolder, childFolder}}, nil)
gvk := schema.GroupVersionKind{Group: resources.DashboardResource.Group, Kind: "Dashboard"}
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{items: []unstructured.Unstructured{dashboard}}, resources.DashboardResource, nil)
repoResources := resources.NewMockRepositoryResources(t)
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool { return obj.GetName() == "dashboard-in-nested-folder" }),
mock.MatchedBy(func(opts resources.WriteOptions) bool { return opts.Path == "grafana/team-a/subteam" })).
Return("grafana/team-a/subteam/dashboard-in-nested-folder.json", nil)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "dashboard-in-nested-folder" && r.Action == repository.FileActionCreated
})).Return()
progress.On("TooManyErrors").Return(nil)
return resourceClients, repoResources, progress
},
options: provisioningV0.ExportJobOptions{
Path: "grafana",
Branch: "feature/branch",
Resources: []provisioningV0.ResourceRef{{Name: "dashboard-in-nested-folder", Kind: "Dashboard", Group: resources.DashboardResource.Group}},
},
},
{
name: "folder client error",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(nil, fmt.Errorf("folder client error"))
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
return resourceClients, nil, progress
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{{Name: "dashboard-1", Kind: "Dashboard", Group: resources.DashboardResource.Group}},
},
wantErr: "get folder client: folder client error",
},
{
name: "resource not found",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(&mockDynamicInterface{}, nil)
gvk := schema.GroupVersionKind{Group: resources.DashboardResource.Group, Kind: "Dashboard"}
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{}, resources.DashboardResource, nil)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "non-existent-dashboard" && r.Error != nil && r.Error.Error() == "get resource dashboard.grafana.app/Dashboard/non-existent-dashboard: no items found"
})).Return()
progress.On("TooManyErrors").Return(nil)
return resourceClients, nil, progress
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{{Name: "non-existent-dashboard", Kind: "Dashboard", Group: resources.DashboardResource.Group}},
},
},
{
name: "dashboard version conversion",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
v1Dashboard := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": "Dashboard",
"metadata": map[string]interface{}{"name": "v2-dashboard"},
"status": map[string]interface{}{
"conversion": map[string]interface{}{"failed": true, "storedVersion": "v2alpha1"},
},
},
}
v2Dashboard := createV2DashboardObject("v2-dashboard", "v2alpha1")
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(&mockDynamicInterface{}, nil)
gvk := schema.GroupVersionKind{Group: resources.DashboardResource.Group, Kind: "Dashboard"}
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{items: []unstructured.Unstructured{v1Dashboard}}, resources.DashboardResource, nil)
v2GVR := schema.GroupVersionResource{Group: resources.DashboardResource.Group, Version: "v2alpha1", Resource: resources.DashboardResource.Resource}
resourceClients.On("ForResource", mock.Anything, v2GVR).Return(&mockDynamicInterface{items: []unstructured.Unstructured{v2Dashboard}}, gvk, nil)
repoResources := resources.NewMockRepositoryResources(t)
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
return obj.GetName() == "v2-dashboard" && obj.GetAPIVersion() == "dashboard.grafana.app/v2alpha1"
}),
mock.Anything).Return("grafana/v2-dashboard.json", nil)
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "v2-dashboard" && r.Action == repository.FileActionCreated
})).Return()
progress.On("TooManyErrors").Return(nil)
return resourceClients, repoResources, progress
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{{Name: "v2-dashboard", Kind: "Dashboard", Group: resources.DashboardResource.Group}},
},
},
{
name: "too many errors",
setupMocks: func(t *testing.T) (*resources.MockResourceClients, *resources.MockRepositoryResources, *jobs.MockJobProgressRecorder) {
dashboard := createDashboardObject("dashboard-1")
resourceClients := resources.NewMockResourceClients(t)
resourceClients.On("Folder", mock.Anything).Return(&mockDynamicInterface{}, nil)
gvk := schema.GroupVersionKind{Group: resources.DashboardResource.Group, Kind: "Dashboard"}
resourceClients.On("ForKind", mock.Anything, gvk).Return(&mockDynamicInterface{items: []unstructured.Unstructured{dashboard}}, resources.DashboardResource, nil)
repoResources := resources.NewMockRepositoryResources(t)
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.Anything, mock.Anything).Return("", fmt.Errorf("write error"))
progress := jobs.NewMockJobProgressRecorder(t)
progress.On("SetMessage", mock.Anything, mock.Anything).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(r jobs.JobResourceResult) bool {
return r.Name == "dashboard-1" && r.Action == repository.FileActionIgnored && r.Error != nil
})).Return()
progress.On("TooManyErrors").Return(fmt.Errorf("too many errors"))
return resourceClients, repoResources, progress
},
options: provisioningV0.ExportJobOptions{
Resources: []provisioningV0.ResourceRef{{Name: "dashboard-1", Kind: "Dashboard", Group: resources.DashboardResource.Group}},
},
wantErr: "too many errors",
},
}
folderItems := []unstructured.Unstructured{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resourceClients, repoResources, progress := tt.setupMocks(t)
dashboard := createDashboardObject("dashboard-1")
err := ExportSpecificResources(context.Background(), "test-repo", tt.options, resourceClients, repoResources, progress)
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
progress.On("SetMessage", mock.Anything, "exporting specific resources").Return()
progress.On("SetMessage", mock.Anything, "loading folder tree from API server").Return()
progress.On("SetMessage", mock.Anything, mock.AnythingOfType("string")).Return().Maybe()
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
return result.Name == "dashboard-1" &&
result.Action == repository.FileActionIgnored &&
result.Error != nil
})).Return()
progress.On("TooManyErrors").Return(fmt.Errorf("too many errors"))
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
if tt.assertResults != nil {
tt.assertResults(t, resourceClients, repoResources, progress)
}
})
}
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients) {
folderClient := &mockDynamicInterface{items: folderItems}
resourceClients.On("Folder", mock.Anything).Return(folderClient, nil)
dashboardClient := &mockDynamicInterface{
items: []unstructured.Unstructured{dashboard},
}
gvk := schema.GroupVersionKind{
Group: resources.DashboardResource.Group,
Kind: "Dashboard",
}
resourceClients.On("ForKind", mock.Anything, gvk).Return(dashboardClient, resources.DashboardResource, nil)
repoResources.On("WriteResourceFileFromObject", mock.Anything,
mock.Anything,
mock.Anything).Return("", fmt.Errorf("write error"))
}
err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources)
require.EqualError(t, err, "too many errors")
}