fix: delete subfolder dangling panels (#113419)

* fix: delete subfolder dangling panels and error if used

* chore: add observation about library panel DeleteInFolders

- logs folders UIDs on DeleteInFolders error

* chore: add integration test for blocking library panel deletion and handling dangling library panels

* chore: fix integration test on mode 4 and 5
This commit is contained in:
Rafael Bortolon Paulovic
2025-11-06 13:56:32 +01:00
committed by GitHub
parent fd14d4a5ed
commit 7b3145a3c1
4 changed files with 327 additions and 14 deletions
@@ -644,6 +644,21 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder)
}
libraryPanelSrv, ok := s.registry[entity.StandardKindLibraryPanel]
if !ok {
return folder.ErrInternal.Errorf("no library panel service found in registry")
}
// /* TODO: after a decision regarding folder deletion permissions has been made
// (https://github.com/grafana/grafana-enterprise/issues/5144),
// remove the following call to DeleteInFolders
// and remove "user" from the signature of DeleteInFolder in the folder RegistryService.
// Context: https://github.com/grafana/grafana/pull/69149#discussion_r1235057903
// */
// Obs: DeleteInFolders only deletes dangling library panels (not linked to any dashboard) and throws errors if there are connections
if err := libraryPanelSrv.DeleteInFolders(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil {
s.log.Error("failed to delete dangling library panels in folders", "error", err, "folders", strings.Join(folders, ","))
return err
}
// We need a list of dashboard uids inside the folder to delete related dashboards & public dashboards -
// we cannot use the dashboard service directly due to circular dependencies, so use the search client to get the dashboards
request := &resourcepb.ResourceSearchRequest{
@@ -17,6 +17,7 @@ import (
clientrest "k8s.io/client-go/rest"
folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/bus"
@@ -33,6 +34,9 @@ import (
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/publicdashboards"
"github.com/grafana/grafana/pkg/services/search/model"
@@ -209,6 +213,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
{Action: dashboards.ActionFoldersDelete, Scope: dashboards.ScopeFoldersAll},
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
{Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll},
{Action: accesscontrol.ActionLibraryPanelsDelete, Scope: dashboards.ScopeFoldersAll},
}),
}}
@@ -219,6 +224,16 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
}
mockDashboardService := dashboards.NewFakeDashboardService(t)
mockFolderService := foldertest.NewFakeService()
elementService := libraryelements.ProvideService(cfg, db, routing.NewRouteRegister(), mockFolderService, featuremgmt.WithFeatures(), actest.FakeAccessControl{ExpectedEvaluate: true}, mockDashboardService, nil, nil)
lps := librarypanels.LibraryPanelService{
Cfg: cfg,
SQLStore: db,
LibraryElementService: elementService,
FolderService: mockFolderService,
}
publicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
fakeK8sClient := new(client.MockK8sHandler)
@@ -237,6 +252,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
}
require.NoError(t, folderService.RegisterService(alertingStore))
require.NoError(t, folderService.RegisterService(lps))
t.Run("Folder service tests", func(t *testing.T) {
t.Run("Given user has no permissions", func(t *testing.T) {
@@ -877,6 +893,17 @@ func TestIntegrationDeleteFoldersFromApiServer(t *testing.T) {
}
require.NoError(t, service.RegisterService(alertingStore))
mockDashboardService := dashboards.NewFakeDashboardService(t)
mockFolderService := foldertest.NewFakeService()
elementService := libraryelements.ProvideService(cfg, db, routing.NewRouteRegister(), mockFolderService, featuremgmt.WithFeatures(), actest.FakeAccessControl{ExpectedEvaluate: true}, mockDashboardService, nil, nil)
lps := librarypanels.LibraryPanelService{
Cfg: cfg,
SQLStore: db,
LibraryElementService: elementService,
FolderService: mockFolderService,
}
require.NoError(t, service.RegisterService(lps))
t.Run("Should delete folder", func(t *testing.T) {
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{}).Return(nil).Once()
dashboardK8sclient.On("Search", mock.Anything, int64(1), mock.Anything).Return(&resourcepb.ResourceSearchResponse{Results: &resourcepb.ResourceTable{}}, nil).Once()