diff --git a/pkg/registry/apis/provisioning/jobs/export/resources.go b/pkg/registry/apis/provisioning/jobs/export/resources.go index 535649f58b5..6d4dedb639a 100644 --- a/pkg/registry/apis/provisioning/jobs/export/resources.go +++ b/pkg/registry/apis/provisioning/jobs/export/resources.go @@ -105,11 +105,17 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, // ExportSpecificResources exports a list of specific resources identified by ResourceRef entries. // It validates that resources are not folders, are supported, and are unmanaged. -func ExportSpecificResources(ctx context.Context, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error { +// Specific resource export only works with folder sync targets, not instance sync targets. +func ExportSpecificResources(ctx context.Context, repoConfig *provisioning.Repository, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error { if len(options.Resources) == 0 { return errors.New("no resources specified for export") } + // Validate that specific resource export is only used with folder sync targets + if repoConfig.Spec.Sync.Target != provisioning.SyncTargetTypeFolder { + return fmt.Errorf("specific resource export is only supported for folder sync targets, but repository has target type '%s'", repoConfig.Spec.Sync.Target) + } + progress.SetMessage(ctx, "exporting specific resources") tree, err := loadUnmanagedFolderTree(ctx, clients, progress) diff --git a/pkg/registry/apis/provisioning/jobs/export/resources_specific_test.go b/pkg/registry/apis/provisioning/jobs/export/resources_specific_test.go index 1343502bb26..39edb0f5e33 100644 --- a/pkg/registry/apis/provisioning/jobs/export/resources_specific_test.go +++ b/pkg/registry/apis/provisioning/jobs/export/resources_specific_test.go @@ -17,6 +17,28 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) +// Helper function to create a repository config with folder sync target +func createFolderSyncRepository() *provisioningV0.Repository { + return &provisioningV0.Repository{ + Spec: provisioningV0.RepositorySpec{ + Sync: provisioningV0.SyncOptions{ + Target: provisioningV0.SyncTargetTypeFolder, + }, + }, + } +} + +// Helper function to create a repository config with instance sync target +func createInstanceSyncRepository() *provisioningV0.Repository { + return &provisioningV0.Repository{ + Spec: provisioningV0.RepositorySpec{ + Sync: provisioningV0.SyncOptions{ + Target: provisioningV0.SyncTargetTypeInstance, + }, + }, + } +} + // Helper function to create folder objects func createFolderObject(name, uid, parentFolderUID string) unstructured.Unstructured { folder := unstructured.Unstructured{ @@ -50,7 +72,7 @@ func createDashboardObjectWithFolder(name, folderID string) unstructured.Unstruc } // 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 { +func runExportSpecificResourcesTest(t *testing.T, repoConfig *provisioningV0.Repository, 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) @@ -64,7 +86,7 @@ func runExportSpecificResourcesTest(t *testing.T, resourceRefs []provisioningV0. Resources: resourceRefs, } - err := ExportSpecificResources(context.Background(), options, resourceClients, repoResources, mockProgress) + err := ExportSpecificResources(context.Background(), repoConfig, options, resourceClients, repoResources, mockProgress) mockProgress.AssertExpectations(t) repoResources.AssertExpectations(t) @@ -74,6 +96,8 @@ func runExportSpecificResourcesTest(t *testing.T, resourceRefs []provisioningV0. } func TestExportSpecificResources_Success(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "dashboard-1", @@ -148,11 +172,13 @@ func TestExportSpecificResources_Success(t *testing.T) { })).Return("grafana/dashboard-2.json", nil) } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.NoError(t, err) } func TestExportSpecificResources_EmptyResources(t *testing.T) { + repoConfig := createFolderSyncRepository() + options := provisioningV0.ExportJobOptions{ Path: "grafana", Branch: "feature/branch", @@ -163,11 +189,36 @@ func TestExportSpecificResources_EmptyResources(t *testing.T) { repoResources := resources.NewMockRepositoryResources(t) mockProgress := jobs.NewMockJobProgressRecorder(t) - err := ExportSpecificResources(context.Background(), options, resourceClients, repoResources, mockProgress) + err := ExportSpecificResources(context.Background(), repoConfig, options, resourceClients, repoResources, mockProgress) require.EqualError(t, err, "no resources specified for export") } +func TestExportSpecificResources_RejectsInstanceSyncTarget(t *testing.T) { + repoConfig := createInstanceSyncRepository() + + options := provisioningV0.ExportJobOptions{ + Path: "grafana", + Branch: "feature/branch", + Resources: []provisioningV0.ResourceRef{ + { + Name: "dashboard-1", + Kind: "Dashboard", + Group: resources.DashboardResource.Group, + }, + }, + } + + resourceClients := resources.NewMockResourceClients(t) + repoResources := resources.NewMockRepositoryResources(t) + mockProgress := jobs.NewMockJobProgressRecorder(t) + + err := ExportSpecificResources(context.Background(), repoConfig, options, resourceClients, repoResources, mockProgress) + require.EqualError(t, err, "specific resource export is only supported for folder sync targets, but repository has target type 'instance'") +} + func TestExportSpecificResources_RejectsFolders(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "my-folder", @@ -196,11 +247,13 @@ func TestExportSpecificResources_RejectsFolders(t *testing.T) { // No ForKind or WriteResourceFileFromObject calls expected for folders } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.NoError(t, err) } func TestExportSpecificResources_RejectsManagedResources(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "managed-dashboard", @@ -246,11 +299,13 @@ func TestExportSpecificResources_RejectsManagedResources(t *testing.T) { // No WriteResourceFileFromObject call expected for managed resources } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.NoError(t, err) } func TestExportSpecificResources_RejectsUnsupportedResources(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "some-resource", @@ -292,11 +347,13 @@ func TestExportSpecificResources_RejectsUnsupportedResources(t *testing.T) { // No WriteResourceFileFromObject call expected for unsupported resources } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.NoError(t, err) } func TestExportSpecificResources_FolderPathResolution(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "dashboard-in-nested-folder", @@ -346,11 +403,13 @@ func TestExportSpecificResources_FolderPathResolution(t *testing.T) { })).Return("grafana/team-a/subteam/dashboard-in-nested-folder.json", nil) } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.NoError(t, err) } func TestExportSpecificResources_FolderClientError(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "dashboard-1", @@ -368,11 +427,13 @@ func TestExportSpecificResources_FolderClientError(t *testing.T) { resourceClients.On("Folder", mock.Anything).Return(nil, fmt.Errorf("folder client error")) } - err := runExportSpecificResourcesTest(t, resourceRefs, nil, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, nil, setupProgress, setupResources) require.EqualError(t, err, "get folder client: folder client error") } func TestExportSpecificResources_ResourceNotFound(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "non-existent-dashboard", @@ -410,11 +471,13 @@ func TestExportSpecificResources_ResourceNotFound(t *testing.T) { resourceClients.On("ForKind", mock.Anything, gvk).Return(dashboardClient, resources.DashboardResource, nil) } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.NoError(t, err) } func TestExportSpecificResources_DashboardVersionConversion(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "v2-dashboard", @@ -490,11 +553,13 @@ func TestExportSpecificResources_DashboardVersionConversion(t *testing.T) { })).Return("grafana/v2-dashboard.json", nil) } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.NoError(t, err) } func TestExportSpecificResources_TooManyErrors(t *testing.T) { + repoConfig := createFolderSyncRepository() + resourceRefs := []provisioningV0.ResourceRef{ { Name: "dashboard-1", @@ -537,6 +602,6 @@ func TestExportSpecificResources_TooManyErrors(t *testing.T) { mock.Anything).Return("", fmt.Errorf("write error")) } - err := runExportSpecificResourcesTest(t, resourceRefs, folderItems, setupProgress, setupResources) + err := runExportSpecificResourcesTest(t, repoConfig, resourceRefs, folderItems, setupProgress, setupResources) require.EqualError(t, err, "too many errors") } diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go index 3543b687c2d..28804717b22 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -104,7 +104,7 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, if len(options.Resources) > 0 { progress.SetTotal(ctx, len(options.Resources)) progress.StrictMaxErrors(1) // Fail fast on any error during export - return ExportSpecificResources(ctx, *options, clients, repositoryResources, progress) + return ExportSpecificResources(ctx, cfg, *options, clients, repositoryResources, progress) } // Fall back to existing ExportAll behavior for backward compatibility