diff --git a/pkg/registry/apis/folders/register_test.go b/pkg/registry/apis/folders/register_test.go index fe472db4b9f..066f8793776 100644 --- a/pkg/registry/apis/folders/register_test.go +++ b/pkg/registry/apis/folders/register_test.go @@ -183,17 +183,85 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { func TestFolderAPIBuilder_Validate_Delete(t *testing.T) { tests := []struct { name string - statsResponse *resourcepb.ResourceStatsResponse_Stats + statsResponse []*resourcepb.ResourceStatsResponse_Stats wantErr bool }{ { - name: "should allow deletion when folder is empty", - statsResponse: &resourcepb.ResourceStatsResponse_Stats{Count: 0}, + name: "should allow deletion when folder is empty", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 0, Resource: "dashboards"}, + }, }, { - name: "should return folder not empty when the folder is not empty", - statsResponse: &resourcepb.ResourceStatsResponse_Stats{Count: 2}, - wantErr: true, + name: "should return folder not empty when folder contains dashboards", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 2, Resource: "dashboards", Group: "dashboard.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder contains alertrules", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 3, Resource: "alertrules", Group: "alerting.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder contains library_elements", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 1, Resource: "library_elements", Group: "library.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder contains folders", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 2, Resource: "folders", Group: "folders.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder has mixed resources with validated types", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 10, Resource: "folders", Group: "folders.grafana.app"}, + {Count: 2, Resource: "dashboards", Group: "dashboard.grafana.app"}, + {Count: 5, Resource: "playlists", Group: "playlist.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should return folder not empty when folder has multiple validated resource types", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 1, Resource: "dashboards", Group: "dashboard.grafana.app"}, + {Count: 2, Resource: "alertrules", Group: "alerting.grafana.app"}, + {Count: 1, Resource: "library_elements", Group: "library.grafana.app"}, + {Count: 1, Resource: "folders", Group: "folders.grafana.app"}, + }, + wantErr: true, + }, + { + name: "should allow deletion when all validated resource types are empty", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 0, Resource: "dashboards", Group: "dashboard.grafana.app"}, + {Count: 0, Resource: "alertrules", Group: "alerting.grafana.app"}, + {Count: 0, Resource: "library_elements", Group: "library.grafana.app"}, + {Count: 0, Resource: "folders", Group: "folders.grafana.app"}, + {Count: 10, Resource: "playlists", Group: "playlist.grafana.app"}, + }, + wantErr: false, + }, + { + name: "should allow deletion when folder only contains non-validated resource types", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{ + {Count: 5, Resource: "playlists", Group: "playlist.grafana.app"}, + {Count: 3, Resource: "other", Group: "other.grafana.app"}, + }, + wantErr: false, + }, + { + name: "should allow deletion when stats array is empty", + statsResponse: []*resourcepb.ResourceStatsResponse_Stats{}, + wantErr: false, }, } @@ -212,7 +280,7 @@ func TestFolderAPIBuilder_Validate_Delete(t *testing.T) { us := grafanarest.NewMockStorage(t) sm := resource.NewMockResourceClient(t) sm.On("GetStats", mock.Anything, &resourcepb.ResourceStatsRequest{Namespace: obj.Namespace, Folder: obj.Name}).Return( - &resourcepb.ResourceStatsResponse{Stats: []*resourcepb.ResourceStatsResponse_Stats{tt.statsResponse}}, + &resourcepb.ResourceStatsResponse{Stats: tt.statsResponse}, nil, ).Once() diff --git a/pkg/registry/apis/folders/validate.go b/pkg/registry/apis/folders/validate.go index f63583b872c..db8c3b7e0e8 100644 --- a/pkg/registry/apis/folders/validate.go +++ b/pkg/registry/apis/folders/validate.go @@ -144,9 +144,11 @@ func validateOnDelete(ctx context.Context, return fmt.Errorf("could not verify if folder is empty: %v", resp.Error) } + allowedResourceTypes := []string{"alertrules", "dashboards", "library_elements", "folders"} + for _, v := range resp.Stats { - if v.Count > 0 { - return folder.ErrFolderNotEmpty.Errorf("folder is not empty, contains %d %s.%s", v.Count, v.Group, v.Resource) + if slices.Contains(allowedResourceTypes, v.Resource) && v.Count > 0 { + return folder.ErrFolderNotEmpty.Errorf("folder is not empty, contains %d %s", v.Count, v.Resource) } } return nil diff --git a/pkg/registry/apis/folders/validate_test.go b/pkg/registry/apis/folders/validate_test.go index 2bed196ddce..33fda7ea30f 100644 --- a/pkg/registry/apis/folders/validate_test.go +++ b/pkg/registry/apis/folders/validate_test.go @@ -320,7 +320,7 @@ func TestValidateDelete(t *testing.T) { }, }, }, { - name: "stats error", + name: "stats error - nil stats", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -331,7 +331,7 @@ func TestValidateDelete(t *testing.T) { }, expectedErr: "could not verify if folder is empty", }, { - name: "stats error", + name: "stats error - search error", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -342,7 +342,7 @@ func TestValidateDelete(t *testing.T) { }, expectedErr: "error running stats", }, { - name: "stats error", + name: "stats error - error result", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -357,7 +357,64 @@ func TestValidateDelete(t *testing.T) { }, expectedErr: "could not verify if folder is empty", }, { - name: "folder not empty", + name: "folder not empty - contains dashboards", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "dashboard.grafana.app", + Resource: "dashboards", + Count: 10, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder not empty - contains alertrules", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "alerting.grafana.app", + Resource: "alertrules", + Count: 5, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder not empty - contains library_elements", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "library.grafana.app", + Resource: "library_elements", + Count: 3, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder not empty - contains folders", folder: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "nnn", @@ -369,7 +426,54 @@ func TestValidateDelete(t *testing.T) { { Group: "folders.grafana.app", Resource: "folders", - Count: 10, // not empty + Count: 2, // not empty + }, + }, + }, + }, + expectedErr: "[folder.not-empty]", + }, { + name: "folder can be deleted when it only contains non-validated resource types", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "playlist.grafana.app", + Resource: "playlists", + Count: 10, // has content but not a validated resource type + }, + { + Group: "other.grafana.app", + Resource: "other", + Count: 5, // has content but not a validated resource type + }, + }, + }, + }, + }, { + name: "folder not empty - mixed resources with validated types", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nnn", + }, + }, + searcher: &mockSearchClient{ + stats: &resourcepb.ResourceStatsResponse{ + Stats: []*resourcepb.ResourceStatsResponse_Stats{ + { + Group: "folders.grafana.app", + Resource: "folders", + Count: 10, // now validated + }, + { + Group: "dashboard.grafana.app", + Resource: "dashboards", + Count: 2, // validated and has content }, }, },