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