diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index f77dd9b2af0..ff13b96f64e 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -24,7 +24,7 @@ type DashboardService interface { ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error) - CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error) + CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*Dashboard, error) } @@ -74,6 +74,6 @@ type Store interface { Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) // CountDashboardsInFolder returns the number of dashboards associated with // the given parent folder ID. - CountDashboardsInFolder(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) - DeleteDashboardsInFolder(ctx context.Context, request *DeleteDashboardsInFolderRequest) error + CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) + DeleteDashboardsInFolders(ctx context.Context, request *DeleteDashboardsInFolderRequest) error } diff --git a/pkg/services/dashboards/dashboard_provisioning_mock.go b/pkg/services/dashboards/dashboard_provisioning_mock.go index 9931ad15bda..a743752fcd5 100644 --- a/pkg/services/dashboards/dashboard_provisioning_mock.go +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package dashboards @@ -18,10 +18,6 @@ type FakeDashboardProvisioning struct { func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error { ret := _m.Called(ctx, cmd) - if len(ret) == 0 { - panic("no return value specified for DeleteOrphanedProvisionedDashboards") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok { r0 = rf(ctx, cmd) @@ -36,10 +32,6 @@ func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx con func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error { ret := _m.Called(ctx, dashboardID, orgID) - if len(ret) == 0 { - panic("no return value specified for DeleteProvisionedDashboard") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { r0 = rf(ctx, dashboardID, orgID) @@ -54,10 +46,6 @@ func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Cont func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) { ret := _m.Called(ctx, name) - if len(ret) == 0 { - panic("no return value specified for GetProvisionedDashboardData") - } - var r0 []*DashboardProvisioning var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) ([]*DashboardProvisioning, error)); ok { @@ -84,10 +72,6 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Con func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) { ret := _m.Called(ctx, dashboardID) - if len(ret) == 0 { - panic("no return value specified for GetProvisionedDashboardDataByDashboardID") - } - var r0 *DashboardProvisioning var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64) (*DashboardProvisioning, error)); ok { @@ -114,10 +98,6 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ct func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) { ret := _m.Called(ctx, orgID, dashboardUID) - if len(ret) == 0 { - panic("no return value specified for GetProvisionedDashboardDataByDashboardUID") - } - var r0 *DashboardProvisioning var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*DashboardProvisioning, error)); ok { @@ -144,10 +124,6 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(c func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *folder.CreateFolderCommand) (*folder.Folder, error) { ret := _m.Called(_a0, _a1) - if len(ret) == 0 { - panic("no return value specified for SaveFolderForProvisionedDashboards") - } - var r0 *folder.Folder var r1 error if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand) (*folder.Folder, error)); ok { @@ -174,10 +150,6 @@ func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 cont func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) { ret := _m.Called(ctx, dto, provisioning) - if len(ret) == 0 { - panic("no return value specified for SaveProvisionedDashboard") - } - var r0 *Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) (*Dashboard, error)); ok { @@ -204,10 +176,6 @@ func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Contex func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, dashboardID int64) error { ret := _m.Called(ctx, dashboardID) - if len(ret) == 0 { - panic("no return value specified for UnprovisionDashboard") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { r0 = rf(ctx, dashboardID) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 828842e2a37..da3095b448d 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package dashboards @@ -20,10 +20,6 @@ type FakeDashboardService struct { func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) { ret := _m.Called(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) - if len(ret) == 0 { - panic("no return value specified for BuildSaveDashboardCommand") - } - var r0 *SaveDashboardCommand var r1 error if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool, bool) (*SaveDashboardCommand, error)); ok { @@ -46,27 +42,23 @@ func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, d return r0, r1 } -// CountInFolder provides a mock function with given fields: ctx, orgID, folderUID, user -func (_m *FakeDashboardService) CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error) { - ret := _m.Called(ctx, orgID, folderUID, user) - - if len(ret) == 0 { - panic("no return value specified for CountInFolder") - } +// CountInFolders provides a mock function with given fields: ctx, orgID, folderUIDs, user +func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) { + ret := _m.Called(ctx, orgID, folderUIDs, user) var r0 int64 var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64, string, identity.Requester) (int64, error)); ok { - return rf(ctx, orgID, folderUID, user) + if rf, ok := ret.Get(0).(func(context.Context, int64, []string, identity.Requester) (int64, error)); ok { + return rf(ctx, orgID, folderUIDs, user) } - if rf, ok := ret.Get(0).(func(context.Context, int64, string, identity.Requester) int64); ok { - r0 = rf(ctx, orgID, folderUID, user) + if rf, ok := ret.Get(0).(func(context.Context, int64, []string, identity.Requester) int64); ok { + r0 = rf(ctx, orgID, folderUIDs, user) } else { r0 = ret.Get(0).(int64) } - if rf, ok := ret.Get(1).(func(context.Context, int64, string, identity.Requester) error); ok { - r1 = rf(ctx, orgID, folderUID, user) + if rf, ok := ret.Get(1).(func(context.Context, int64, []string, identity.Requester) error); ok { + r1 = rf(ctx, orgID, folderUIDs, user) } else { r1 = ret.Error(1) } @@ -78,10 +70,6 @@ func (_m *FakeDashboardService) CountInFolder(ctx context.Context, orgID int64, func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { ret := _m.Called(ctx, dashboardId, orgId) - if len(ret) == 0 { - panic("no return value specified for DeleteDashboard") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { r0 = rf(ctx, dashboardId, orgId) @@ -96,10 +84,6 @@ func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for FindDashboards") - } - var r0 []DashboardSearchProjection var r1 error if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)); ok { @@ -126,10 +110,6 @@ func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindP func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboard") - } - var r0 *Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) (*Dashboard, error)); ok { @@ -156,10 +136,6 @@ func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDash func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboardTags") - } - var r0 []*DashboardTagCloudItem var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error)); ok { @@ -186,10 +162,6 @@ func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *Get func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboardUIDByID") - } - var r0 *DashboardRef var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) (*DashboardRef, error)); ok { @@ -216,10 +188,6 @@ func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query * func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboards") - } - var r0 []*Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) ([]*Dashboard, error)); ok { @@ -246,10 +214,6 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDas func (_m *FakeDashboardService) GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*Dashboard, error) { ret := _m.Called(ctx, user) - if len(ret) == 0 { - panic("no return value specified for GetDashboardsSharedWithUser") - } - var r0 []*Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) ([]*Dashboard, error)); ok { @@ -276,10 +240,6 @@ func (_m *FakeDashboardService) GetDashboardsSharedWithUser(ctx context.Context, func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) { ret := _m.Called(ctx, dto) - if len(ret) == 0 { - panic("no return value specified for ImportDashboard") - } - var r0 *Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) (*Dashboard, error)); ok { @@ -306,10 +266,6 @@ func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDa func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) { ret := _m.Called(ctx, dto, allowUiUpdate) - if len(ret) == 0 { - panic("no return value specified for SaveDashboard") - } - var r0 *Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool) (*Dashboard, error)); ok { @@ -336,10 +292,6 @@ func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDash func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for SearchDashboards") - } - var r0 model.HitList var r1 error if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) (model.HitList, error)); ok { diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 75d78636ef2..80beba5921a 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "time" "xorm.io/xorm" @@ -657,7 +658,12 @@ func (d *dashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.De } func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, sess *db.Session, emitEntityEvent bool) error { - dashboard := dashboards.Dashboard{ID: cmd.ID, OrgID: cmd.OrgID} + dashboard := dashboards.Dashboard{OrgID: cmd.OrgID} + if cmd.UID != "" { + dashboard.UID = cmd.UID + } else { + dashboard.ID = cmd.ID + } has, err := sess.Get(&dashboard) if err != nil { return err @@ -1008,42 +1014,58 @@ func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards // CountDashboardsInFolder returns a count of all dashboards associated with the // given parent folder ID. -// -// This will be updated to take CountDashboardsInFolderQuery as an argument and -// lookup dashboards using the ParentFolderUID when dashboards are associated with a parent folder UID instead of ID. -func (d *dashboardStore) CountDashboardsInFolder( +func (d *dashboardStore) CountDashboardsInFolders( ctx context.Context, req *dashboards.CountDashboardsInFolderRequest) (int64, error) { + if len(req.FolderUIDs) == 0 { + return 0, nil + } var count int64 - var err error - err = d.store.WithDbSession(ctx, func(sess *db.Session) error { + err := d.store.WithDbSession(ctx, func(sess *db.Session) error { metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - session := sess.In("folder_id", req.FolderID).In("org_id", req.OrgID). - In("is_folder", d.store.GetDialect().BooleanStr(false)) - count, err = session.Count(&dashboards.Dashboard{}) + s := strings.Builder{} + args := make([]any, 0, 3) + s.WriteString("SELECT COUNT(*) FROM dashboard WHERE ") + if len(req.FolderUIDs) == 1 && req.FolderUIDs[0] == "" { + s.WriteString("folder_uid IS NULL") + } else { + s.WriteString(fmt.Sprintf("folder_uid IN (%s)", strings.Repeat("?,", len(req.FolderUIDs)-1)+"?")) + for _, folderUID := range req.FolderUIDs { + args = append(args, folderUID) + } + } + s.WriteString(" AND org_id = ? AND is_folder = ?") + args = append(args, req.OrgID, d.store.GetDialect().BooleanStr(false)) + sql := s.String() + _, err := sess.SQL(sql, args...).Get(&count) return err }) return count, err } -func (d *dashboardStore) DeleteDashboardsInFolder( +func (d *dashboardStore) DeleteDashboardsInFolders( ctx context.Context, req *dashboards.DeleteDashboardsInFolderRequest) error { return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - dashboard := dashboards.Dashboard{OrgID: req.OrgID} - has, err := sess.Where("uid = ? AND org_id = ?", req.FolderUID, req.OrgID).Get(&dashboard) - if err != nil { - return err - } - if !has { - return dashboards.ErrFolderNotFound - } + // TODO delete all dashboards in the folder in a bulk query + for _, folderUID := range req.FolderUIDs { + dashboard := dashboards.Dashboard{OrgID: req.OrgID} + has, err := sess.Where("org_id = ? AND uid = ?", req.OrgID, folderUID).Get(&dashboard) + if err != nil { + return err + } + if !has { + return dashboards.ErrFolderNotFound + } - if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil { - return err - } + if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil { + return err + } - _, err = sess.Where("folder_id = ? AND org_id = ? AND is_folder = ?", dashboard.ID, dashboard.OrgID, false).Delete(&dashboards.Dashboard{}) - return err + _, err = sess.Where("folder_id = ? AND org_id = ? AND is_folder = ?", dashboard.ID, dashboard.OrgID, false).Delete(&dashboards.Dashboard{}) + if err != nil { + return err + } + } + return nil }) } diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 75e88deeeb9..487bf6a97e7 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -47,6 +47,11 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { var err error dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) + // insertTestDashboard creates the following hierarchy: + // 1 test dash folder + // test dash 23 + // test dash 45 + // test dash 67 savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp") savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp") insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, savedFolder.UID, false, "prod") @@ -470,17 +475,15 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Can count dashboards by parent folder", func(t *testing.T) { setup() // setup() saves one dashboard in the general folder and two in the "savedFolder". - count, err := dashboardStore.CountDashboardsInFolder( + count, err := dashboardStore.CountDashboardsInFolders( context.Background(), - // nolint:staticcheck - &dashboards.CountDashboardsInFolderRequest{FolderID: 0, OrgID: 1}) + &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{""}, OrgID: 1}) require.NoError(t, err) require.Equal(t, int64(1), count) - count, err = dashboardStore.CountDashboardsInFolder( + count, err = dashboardStore.CountDashboardsInFolders( context.Background(), - // nolint:staticcheck - &dashboards.CountDashboardsInFolderRequest{FolderID: savedFolder.ID, OrgID: 1}) + &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1}) require.NoError(t, err) require.Equal(t, int64(2), count) }) @@ -491,16 +494,16 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { _ = insertTestDashboard(t, dashboardStore, "delete me 1", 1, folder.ID, folder.UID, false, "delete this 1") _ = insertTestDashboard(t, dashboardStore, "delete me 2", 1, folder.ID, folder.UID, false, "delete this 2") - err := dashboardStore.DeleteDashboardsInFolder( + err := dashboardStore.DeleteDashboardsInFolders( context.Background(), &dashboards.DeleteDashboardsInFolderRequest{ - FolderUID: folder.UID, - OrgID: 1, + FolderUIDs: []string{folder.UID}, + OrgID: 1, }) require.NoError(t, err) // nolint:staticcheck - count, err := dashboardStore.CountDashboardsInFolder(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderID: 2, OrgID: 1}) + count, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{folder.UID}, OrgID: 1}) require.NoError(t, err) require.Equal(t, count, int64(0)) }) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index e6c78305bfd..7f23ac5a1e5 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -220,6 +220,7 @@ type DashboardProvisioning struct { type DeleteDashboardCommand struct { ID int64 + UID string OrgID int64 ForceDeleteFolderRules bool } @@ -322,9 +323,8 @@ type CountDashboardsInFolderQuery struct { // to the store layer. The FolderID will be replaced with FolderUID when // dashboards are updated with parent folder UIDs. type CountDashboardsInFolderRequest struct { - // Deprecated: use FolderUID instead - FolderID int64 - OrgID int64 + FolderUIDs []string + OrgID int64 } func FromDashboard(dash *Dashboard) *folder.Folder { @@ -345,8 +345,8 @@ func FromDashboard(dash *Dashboard) *folder.Folder { } type DeleteDashboardsInFolderRequest struct { - FolderUID string - OrgID int64 + FolderUIDs []string + OrgID int64 } // diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 3ab42790251..b61042c7424 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -733,19 +733,12 @@ func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *das return dr.dashboardStore.GetDashboardTags(ctx, query) } -func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) { - folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{UID: &folderUID, OrgID: orgID, SignedInUser: u}) - if err != nil { - return 0, err - } - - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - return dr.dashboardStore.CountDashboardsInFolder(ctx, &dashboards.CountDashboardsInFolderRequest{FolderID: folder.ID, OrgID: orgID}) +func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) { + return dr.dashboardStore.CountDashboardsInFolders(ctx, &dashboards.CountDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID}) } -func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) error { - return dr.dashboardStore.DeleteDashboardsInFolder(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUID: folderUID, OrgID: orgID}) +func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) error { + return dr.dashboardStore.DeleteDashboardsInFolders(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID}) } func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index eeb3a62a4ff..468bd2902f6 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -225,21 +225,21 @@ func TestDashboardService(t *testing.T) { }) t.Run("Count dashboards in folder", func(t *testing.T) { - fakeStore.On("CountDashboardsInFolder", mock.Anything, mock.AnythingOfType("*dashboards.CountDashboardsInFolderRequest")).Return(int64(3), nil) + fakeStore.On("CountDashboardsInFolders", mock.Anything, mock.AnythingOfType("*dashboards.CountDashboardsInFolderRequest")).Return(int64(3), nil) folderSvc.ExpectedFolder = &folder.Folder{UID: "i am a folder"} // set up a ctx with signed in user usr := &user.SignedInUser{UserID: 1} ctx := appcontext.WithUser(context.Background(), usr) - count, err := service.CountInFolder(ctx, 1, "i am a folder", usr) + count, err := service.CountInFolders(ctx, 1, []string{"i am a folder"}, usr) require.NoError(t, err) require.Equal(t, int64(3), count) }) t.Run("Delete dashboards in folder", func(t *testing.T) { - args := &dashboards.DeleteDashboardsInFolderRequest{OrgID: 1, FolderUID: "uid"} - fakeStore.On("DeleteDashboardsInFolder", mock.Anything, args).Return(nil).Once() - err := service.DeleteInFolder(context.Background(), 1, "uid", nil) + args := &dashboards.DeleteDashboardsInFolderRequest{OrgID: 1, FolderUIDs: []string{"uid"}} + fakeStore.On("DeleteDashboardsInFolders", mock.Anything, args).Return(nil).Once() + err := service.DeleteInFolders(context.Background(), 1, []string{"uid"}, nil) require.NoError(t, err) }) }) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 71422fd1967..0aafff81b73 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package dashboards @@ -20,10 +20,6 @@ type FakeDashboardStore struct { func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParameters) (*quota.Map, error) { ret := _m.Called(_a0, _a1) - if len(ret) == 0 { - panic("no return value specified for Count") - } - var r0 *quota.Map var r1 error if rf, ok := ret.Get(0).(func(context.Context, *quota.ScopeParameters) (*quota.Map, error)); ok { @@ -46,14 +42,10 @@ func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParamet return r0, r1 } -// CountDashboardsInFolder provides a mock function with given fields: ctx, request -func (_m *FakeDashboardStore) CountDashboardsInFolder(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) { +// CountDashboardsInFolders provides a mock function with given fields: ctx, request +func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) { ret := _m.Called(ctx, request) - if len(ret) == 0 { - panic("no return value specified for CountDashboardsInFolder") - } - var r0 int64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, *CountDashboardsInFolderRequest) (int64, error)); ok { @@ -78,10 +70,6 @@ func (_m *FakeDashboardStore) CountDashboardsInFolder(ctx context.Context, reque func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error { ret := _m.Called(ctx, cmd) - if len(ret) == 0 { - panic("no return value specified for DeleteDashboard") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteDashboardCommand) error); ok { r0 = rf(ctx, cmd) @@ -92,14 +80,10 @@ func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDa return r0 } -// DeleteDashboardsInFolder provides a mock function with given fields: ctx, request -func (_m *FakeDashboardStore) DeleteDashboardsInFolder(ctx context.Context, request *DeleteDashboardsInFolderRequest) error { +// DeleteDashboardsInFolders provides a mock function with given fields: ctx, request +func (_m *FakeDashboardStore) DeleteDashboardsInFolders(ctx context.Context, request *DeleteDashboardsInFolderRequest) error { ret := _m.Called(ctx, request) - if len(ret) == 0 { - panic("no return value specified for DeleteDashboardsInFolder") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteDashboardsInFolderRequest) error); ok { r0 = rf(ctx, request) @@ -114,10 +98,6 @@ func (_m *FakeDashboardStore) DeleteDashboardsInFolder(ctx context.Context, requ func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error { ret := _m.Called(ctx, cmd) - if len(ret) == 0 { - panic("no return value specified for DeleteOrphanedProvisionedDashboards") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok { r0 = rf(ctx, cmd) @@ -132,10 +112,6 @@ func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Co func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for FindDashboards") - } - var r0 []DashboardSearchProjection var r1 error if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)); ok { @@ -162,10 +138,6 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPer func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboard") - } - var r0 *Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) (*Dashboard, error)); ok { @@ -192,10 +164,6 @@ func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashbo func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboardTags") - } - var r0 []*DashboardTagCloudItem var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error)); ok { @@ -222,10 +190,6 @@ func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDa func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboardUIDByID") - } - var r0 *DashboardRef var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) (*DashboardRef, error)); ok { @@ -252,10 +216,6 @@ func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *Ge func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboards") - } - var r0 []*Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) ([]*Dashboard, error)); ok { @@ -282,10 +242,6 @@ func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashb func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) - if len(ret) == 0 { - panic("no return value specified for GetDashboardsByPluginID") - } - var r0 []*Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsByPluginIDQuery) ([]*Dashboard, error)); ok { @@ -312,10 +268,6 @@ func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) { ret := _m.Called(ctx, name) - if len(ret) == 0 { - panic("no return value specified for GetProvisionedDashboardData") - } - var r0 []*DashboardProvisioning var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) ([]*DashboardProvisioning, error)); ok { @@ -342,10 +294,6 @@ func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, n func (_m *FakeDashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) { ret := _m.Called(ctx, dashboardID) - if len(ret) == 0 { - panic("no return value specified for GetProvisionedDataByDashboardID") - } - var r0 *DashboardProvisioning var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64) (*DashboardProvisioning, error)); ok { @@ -372,10 +320,6 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardID(ctx context.Contex func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) { ret := _m.Called(ctx, orgID, dashboardUID) - if len(ret) == 0 { - panic("no return value specified for GetProvisionedDataByDashboardUID") - } - var r0 *DashboardProvisioning var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*DashboardProvisioning, error)); ok { @@ -402,10 +346,6 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(ctx context.Conte func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { ret := _m.Called(ctx, dashID, alerts) - if len(ret) == 0 { - panic("no return value specified for SaveAlerts") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, []*models.Alert) error); ok { r0 = rf(ctx, dashID, alerts) @@ -420,10 +360,6 @@ func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, aler func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) { ret := _m.Called(ctx, cmd) - if len(ret) == 0 { - panic("no return value specified for SaveDashboard") - } - var r0 *Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand) (*Dashboard, error)); ok { @@ -450,10 +386,6 @@ func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd SaveDashboa func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) { ret := _m.Called(ctx, cmd, provisioning) - if len(ret) == 0 { - panic("no return value specified for SaveProvisionedDashboard") - } - var r0 *Dashboard var r1 error if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand, *DashboardProvisioning) (*Dashboard, error)); ok { @@ -480,10 +412,6 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error { ret := _m.Called(ctx, id) - if len(ret) == 0 { - panic("no return value specified for UnprovisionDashboard") - } - var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { r0 = rf(ctx, id) @@ -498,10 +426,6 @@ func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64 func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) { ret := _m.Called(ctx, dashboard, overwrite) - if len(ret) == 0 { - panic("no return value specified for ValidateDashboardBeforeSave") - } - var r0 bool var r1 error if rf, ok := ret.Get(0).(func(context.Context, *Dashboard, bool) (bool, error)); ok { diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 7fb2488675d..78a42724677 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -748,72 +748,62 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e return dashboards.ErrFolderAccessDenied } - result := []string{cmd.UID} + folders := []string{cmd.UID} err = s.db.InTransaction(ctx, func(ctx context.Context) error { - subfolders, err := s.nestedFolderDelete(ctx, cmd) + descendants, err := s.nestedFolderDelete(ctx, cmd) if err != nil { logger.Error("the delete folder on folder table failed with err: ", "error", err) return err } - result = append(result, subfolders...) + folders = append(folders, descendants...) - dashFolders, err := s.dashboardFolderStore.GetFolders(ctx, cmd.OrgID, result) - if err != nil { - return folder.ErrInternal.Errorf("failed to fetch subfolders from dashboard store: %w", err) - } - - for _, foldr := range result { - dashFolder, ok := dashFolders[foldr] - if !ok { - return folder.ErrInternal.Errorf("folder does not exist in dashboard store") - } - - if cmd.ForceDeleteRules { - if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser); err != nil { - return err - } - } else { - alertRuleSrv, ok := s.registry[entity.StandardKindAlertRule] - if !ok { - return folder.ErrInternal.Errorf("no alert rule service found in registry") - } - alertRulesInFolder, err := alertRuleSrv.CountInFolder(ctx, dashFolder.OrgID, dashFolder.UID, cmd.SignedInUser) - if err != nil { - s.log.Error("failed to count alert rules in folder", "error", err) - return err - } - if alertRulesInFolder > 0 { - return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder) - } - } - - if err = s.legacyDelete(ctx, cmd, dashFolder); err != nil { + if cmd.ForceDeleteRules { + if err := s.deleteChildrenInFolder(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil { return err } + } else { + alertRuleSrv, ok := s.registry[entity.StandardKindAlertRule] + if !ok { + return folder.ErrInternal.Errorf("no alert rule service found in registry") + } + alertRulesInFolder, err := alertRuleSrv.CountInFolders(ctx, cmd.OrgID, folders, cmd.SignedInUser) + if err != nil { + s.log.Error("failed to count alert rules in folder", "error", err) + return err + } + if alertRulesInFolder > 0 { + return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder) + } } + + if err = s.legacyDelete(ctx, cmd, folders); err != nil { + return err + } + return nil }) return err } -func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error { +func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error { for _, v := range s.registry { - if err := v.DeleteInFolder(ctx, orgID, folderUID, user); err != nil { + if err := v.DeleteInFolders(ctx, orgID, folderUIDs, user); err != nil { return err } } return nil } -func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderCommand, dashFolder *folder.Folder) error { - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() - // nolint:staticcheck - deleteCmd := dashboards.DeleteDashboardCommand{OrgID: cmd.OrgID, ID: dashFolder.ID, ForceDeleteFolderRules: cmd.ForceDeleteRules} +func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderCommand, folderUIDs []string) error { + // TODO use bulk delete + for _, folderUID := range folderUIDs { + deleteCmd := dashboards.DeleteDashboardCommand{OrgID: cmd.OrgID, UID: folderUID, ForceDeleteFolderRules: cmd.ForceDeleteRules} - if err := s.dashboardStore.DeleteDashboard(ctx, &deleteCmd); err != nil { - return toFolderError(err) + if err := s.dashboardStore.DeleteDashboard(ctx, &deleteCmd); err != nil { + return toFolderError(err) + } } return nil } @@ -907,9 +897,9 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol // the folder store and returns the UIDs for all its descendants. func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFolderCommand) ([]string, error) { logger := s.log.FromContext(ctx) - result := []string{} + descendantUIDs := []string{} if cmd.SignedInUser == nil { - return result, folder.ErrBadRequest.Errorf("missing signed in user") + return descendantUIDs, folder.ErrBadRequest.Errorf("missing signed in user") } _, err := s.Get(ctx, &folder.GetFolderQuery{ @@ -918,31 +908,26 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold SignedInUser: cmd.SignedInUser, }) if err != nil { - return result, err + return descendantUIDs, err } - folders, err := s.store.GetChildren(ctx, folder.GetChildrenQuery{UID: cmd.UID, OrgID: cmd.OrgID}) + descendants, err := s.store.GetDescendants(ctx, cmd.OrgID, cmd.UID) if err != nil { - return result, err - } - for _, f := range folders { - result = append(result, f.UID) - logger.Info("deleting subfolder", "org_id", f.OrgID, "uid", f.UID) - subfolders, err := s.nestedFolderDelete(ctx, &folder.DeleteFolderCommand{UID: f.UID, OrgID: f.OrgID, ForceDeleteRules: cmd.ForceDeleteRules, SignedInUser: cmd.SignedInUser}) - if err != nil { - logger.Error("failed deleting subfolder", "org_id", f.OrgID, "uid", f.UID, "error", err) - return result, err - } - result = append(result, subfolders...) + logger.Error("failed to get descendant folders", "error", err) + return descendantUIDs, err } - logger.Info("deleting folder and its contents", "org_id", cmd.OrgID, "uid", cmd.UID) - err = s.store.Delete(ctx, cmd.UID, cmd.OrgID) + for _, f := range descendants { + descendantUIDs = append(descendantUIDs, f.UID) + } + logger.Info("deleting folder and its descendants", "org_id", cmd.OrgID, "uid", cmd.UID) + toDelete := append(descendantUIDs, cmd.UID) + err = s.store.Delete(ctx, toDelete, cmd.OrgID) if err != nil { logger.Info("failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err) - return result, err + return descendantUIDs, err } - return result, nil + return descendantUIDs, nil } func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) { @@ -950,54 +935,36 @@ func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescenda if q.SignedInUser == nil { return nil, folder.ErrBadRequest.Errorf("missing signed-in user") } - if *q.UID == "" { + if q.UID == nil || *q.UID == "" { return nil, folder.ErrBadRequest.Errorf("missing UID") } if q.OrgID < 1 { return nil, folder.ErrBadRequest.Errorf("invalid orgID") } - result := []string{*q.UID} + folders := []string{*q.UID} countsMap := make(folder.DescendantCounts, len(s.registry)+1) if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { - subfolders, err := s.getNestedFolders(ctx, q.OrgID, *q.UID) + descendantFolders, err := s.store.GetDescendants(ctx, q.OrgID, *q.UID) if err != nil { - logger.Error("failed to get subfolders", "error", err) + logger.Error("failed to get descendant folders", "error", err) return nil, err } - result = append(result, subfolders...) - countsMap[entity.StandardKindFolder] = int64(len(subfolders)) + for _, f := range descendantFolders { + folders = append(folders, f.UID) + } + countsMap[entity.StandardKindFolder] = int64(len(descendantFolders)) } for _, v := range s.registry { - for _, folder := range result { - c, err := v.CountInFolder(ctx, q.OrgID, folder, q.SignedInUser) - if err != nil { - logger.Error("failed to count folder descendants", "error", err) - return nil, err - } - countsMap[v.Kind()] += c - } - } - return countsMap, nil -} - -func (s *Service) getNestedFolders(ctx context.Context, orgID int64, uid string) ([]string, error) { - result := []string{} - folders, err := s.store.GetChildren(ctx, folder.GetChildrenQuery{UID: uid, OrgID: orgID}) - if err != nil { - return nil, err - } - - for _, f := range folders { - result = append(result, f.UID) - subfolders, err := s.getNestedFolders(ctx, f.OrgID, f.UID) + c, err := v.CountInFolders(ctx, q.OrgID, folders, q.SignedInUser) if err != nil { + logger.Error("failed to count folder descendants", "error", err) return nil, err } - result = append(result, subfolders...) + countsMap[v.Kind()] = c } - return result, nil + return countsMap, nil } // buildSaveDashboardCommand is a simplified version on DashboardServiceImpl.buildSaveDashboardCommand diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index c35cc457500..85ba55c66c0 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -259,7 +259,6 @@ func TestIntegrationFolderService(t *testing.T) { t.Run("When deleting folder by uid should not return access denied error", func(t *testing.T) { f := folder.NewFolder(util.GenerateShortUID(), "") f.UID = util.GenerateShortUID() - folderStore.On("GetFolders", mock.Anything, orgID, []string{f.UID}).Return(map[string]*folder.Folder{f.UID: f}, nil) folderStore.On("GetFolderByUID", mock.Anything, orgID, f.UID).Return(f, nil) var actualCmd *dashboards.DeleteDashboardCommand @@ -458,7 +457,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { t.Cleanup(func() { guardian.New = origNewGuardian for _, ancestor := range ancestors { - err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID) + err := serviceWithFlagOn.store.Delete(context.Background(), []string{ancestor.UID}, orgID) assert.NoError(t, err) } }) @@ -538,7 +537,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { t.Cleanup(func() { guardian.New = origNewGuardian for _, ancestor := range ancestors { - err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID) + err := serviceWithFlagOn.store.Delete(context.Background(), []string{ancestor.UID}, orgID) assert.NoError(t, err) } }) @@ -1392,17 +1391,12 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { t.Cleanup(func() { guardian.New = origNewGuardian - for _, ancestor := range ancestorFoldersWithPermissions { - err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID) - assert.NoError(t, err) - } - }) - t.Cleanup(func() { - guardian.New = origNewGuardian - for _, ancestor := range ancestorFoldersWithoutPermissions { - err := serviceWithFlagOn.store.Delete(context.Background(), ancestor.UID, orgID) - assert.NoError(t, err) + toDelete := make([]string, 0, len(ancestorFoldersWithPermissions)+len(ancestorFoldersWithoutPermissions)) + for _, ancestor := range append(ancestorFoldersWithPermissions, ancestorFoldersWithoutPermissions...) { + toDelete = append(toDelete, ancestor.UID) } + err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID) + assert.NoError(t, err) }) }) @@ -1435,14 +1429,12 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { t.Cleanup(func() { guardian.New = origNewGuardian - for _, f := range tree1 { - err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID) - assert.NoError(t, err) - } - for _, f := range tree2 { - err := serviceWithFlagOn.store.Delete(context.Background(), f.UID, orgID) - assert.NoError(t, err) + toDelete := make([]string, 0, len(tree1)+len(tree2)) + for _, f := range append(tree1, tree2...) { + toDelete = append(toDelete, f.UID) } + err := serviceWithFlagOn.store.Delete(context.Background(), toDelete, orgID) + assert.NoError(t, err) }) testCases := []struct { diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 512accdd93a..8796487400c 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -83,11 +83,20 @@ func (ss *sqlStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) return foldr.WithURL(), err } -func (ss *sqlStore) Delete(ctx context.Context, uid string, orgID int64) error { +func (ss *sqlStore) Delete(ctx context.Context, UIDs []string, orgID int64) error { + if len(UIDs) == 0 { + return nil + } return ss.db.WithDbSession(ctx, func(sess *db.Session) error { - _, err := sess.Exec("DELETE FROM folder WHERE uid=? AND org_id=?", uid, orgID) + s := fmt.Sprintf("DELETE FROM folder WHERE org_id=? AND uid IN (%s)", strings.Repeat("?, ", len(UIDs)-1)+"?") + sqlArgs := make([]any, 0, len(UIDs)+2) + sqlArgs = append(sqlArgs, s, orgID) + for _, uid := range UIDs { + sqlArgs = append(sqlArgs, uid) + } + _, err := sess.Exec(sqlArgs...) if err != nil { - return folder.ErrDatabaseError.Errorf("failed to delete folder: %w", err) + return folder.ErrDatabaseError.Errorf("failed to delete folders: %w", err) } return nil }) @@ -331,6 +340,7 @@ func (ss *sqlStore) getParentsMySQL(ctx context.Context, q folder.GetParentsQuer return util.Reverse(folders), err } +// TODO use a single query to get the height of a folder func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) { height := -1 queue := []string{foldrUID} @@ -460,6 +470,65 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde return folders, nil } +func (ss *sqlStore) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) { + var folders []*folder.Folder + + recursiveQueriesAreSupported, err := ss.db.RecursiveQueriesAreSupported() + if err != nil { + return nil, err + } + switch recursiveQueriesAreSupported { + case true: + recQuery := ` + WITH RECURSIVE RecQry AS ( + SELECT * FROM folder WHERE parent_uid = ? AND org_id = ? + UNION ALL SELECT f.* FROM folder f INNER JOIN RecQry r ON f.parent_uid = r.uid and f.org_id = r.org_id + ) + SELECT * FROM RecQry; + ` + if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + err := sess.SQL(recQuery, ancestor_uid, orgID).Find(&folders) + if err != nil { + return folder.ErrDatabaseError.Errorf("failed to get folder descendants: %w", err) + } + return nil + }); err != nil { + return nil, err + } + default: + // this is suboptimal because results is full table scan on f0 + // but it's the best we can do without recursive CTE + if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { + s := strings.Builder{} + args := make([]any, 0, 1+folder.MaxNestedFolderDepth) + args = append(args, orgID) + s.WriteString(`SELECT f0.id, f0.org_id, f0.uid, f0.parent_uid, f0.title, f0.description, f0.created, f0.updated`) + s.WriteString(` FROM folder f0`) + s.WriteString(getFullpathJoinsSQL()) + s.WriteString(` WHERE f0.org_id=?`) + s.WriteString(` AND (`) + for i := 1; i <= folder.MaxNestedFolderDepth; i++ { + if i > 1 { + s.WriteString(` OR `) + } + s.WriteString(fmt.Sprintf(`f%d.uid=?`, i)) + args = append(args, ancestor_uid) + } + s.WriteString(`)`) + return sess.SQL(s.String(), args...).Find(&folders) + }); err != nil { + return nil, err + } + } + + // Add URLs + for i, f := range folders { + folders[i] = f.WithURL() + } + + return folders, nil +} + func getFullpathSQL(dialect migrator.Dialect) string { concatCols := make([]string, 0, folder.MaxNestedFolderDepth) concatCols = append(concatCols, "COALESCE(REPLACE(f0.title, '/', '\\/'), '')") diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index 7d210feb2b2..59e354b3e8a 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -64,7 +64,7 @@ func TestIntegrationCreate(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { - err := folderStore.Delete(context.Background(), f.UID, orgID) + err := folderStore.Delete(context.Background(), []string{f.UID}, orgID) require.NoError(t, err) }) @@ -102,7 +102,7 @@ func TestIntegrationCreate(t *testing.T) { assert.NotEmpty(t, parent.URL) t.Cleanup(func() { - err := folderStore.Delete(context.Background(), parent.UID, orgID) + err := folderStore.Delete(context.Background(), []string{parent.UID}, orgID) require.NoError(t, err) }) assertAncestorUIDs(t, folderStore, parent, []string{folder.GeneralFolderUID}) @@ -117,7 +117,7 @@ func TestIntegrationCreate(t *testing.T) { }) require.NoError(t, err) t.Cleanup(func() { - err := folderStore.Delete(context.Background(), f.UID, orgID) + err := folderStore.Delete(context.Background(), []string{f.UID}, orgID) require.NoError(t, err) }) @@ -165,7 +165,7 @@ func TestIntegrationDelete(t *testing.T) { t.Cleanup(func() { for _, uid := range ancestorUIDs[1:] { - err := folderStore.Delete(context.Background(), uid, orgID) + err := folderStore.Delete(context.Background(), []string{uid}, orgID) require.NoError(t, err) } }) @@ -178,7 +178,7 @@ func TestIntegrationDelete(t *testing.T) { */ t.Run("deleting a leaf folder should succeed", func(t *testing.T) { - err := folderStore.Delete(context.Background(), ancestorUIDs[len(ancestorUIDs)-1], orgID) + err := folderStore.Delete(context.Background(), []string{ancestorUIDs[len(ancestorUIDs)-1]}, orgID) require.NoError(t, err) children, err := folderStore.GetChildren(context.Background(), folder.GetChildrenQuery{ @@ -221,7 +221,7 @@ func TestIntegrationUpdate(t *testing.T) { require.NoError(t, err) require.Equal(t, f.ParentUID, parent.UID) t.Cleanup(func() { - err := folderStore.Delete(context.Background(), f.UID, orgID) + err := folderStore.Delete(context.Background(), []string{f.UID}, orgID) require.NoError(t, err) }) @@ -393,7 +393,7 @@ func TestIntegrationGet(t *testing.T) { }) t.Cleanup(func() { - err := folderStore.Delete(context.Background(), f.UID, orgID) + err := folderStore.Delete(context.Background(), []string{f.UID}, orgID) require.NoError(t, err) }) @@ -489,7 +489,7 @@ func TestIntegrationGetParents(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { - err := folderStore.Delete(context.Background(), f.UID, orgID) + err := folderStore.Delete(context.Background(), []string{f.UID}, orgID) require.NoError(t, err) }) @@ -561,7 +561,7 @@ func TestIntegrationGetChildren(t *testing.T) { t.Cleanup(func() { for _, uid := range treeLeaves { - err := folderStore.Delete(context.Background(), uid, orgID) + err := folderStore.Delete(context.Background(), []string{uid}, orgID) require.NoError(t, err) } }) @@ -778,7 +778,7 @@ func TestIntegrationGetFolders(t *testing.T) { t.Cleanup(func() { for _, uid := range uids { - err := folderStore.Delete(context.Background(), uid, orgID) + err := folderStore.Delete(context.Background(), []string{uid}, orgID) require.NoError(t, err) } }) diff --git a/pkg/services/folder/folderimpl/store.go b/pkg/services/folder/folderimpl/store.go index 4cee3fcf162..d2177ae4bfb 100644 --- a/pkg/services/folder/folderimpl/store.go +++ b/pkg/services/folder/folderimpl/store.go @@ -23,8 +23,8 @@ type store interface { // Create creates a folder and returns the newly-created folder. Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) - // Delete deletes a folder from the folder store. - Delete(ctx context.Context, uid string, orgID int64) error + // Delete folders with the specified UIDs and orgID from the folder store. + Delete(ctx context.Context, UIDs []string, orgID int64) error // Update updates the given folder's UID, Title, and Description (update mode). // If the NewParentUID field is not nil, it updates also the parent UID (move mode). @@ -48,4 +48,6 @@ type store interface { // GetFolders returns folders with given uids GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) + // GetDescendants returns all descendants of a folder + GetDescendants(ctx context.Context, orgID int64, anchestor_uid string) ([]*folder.Folder, error) } diff --git a/pkg/services/folder/folderimpl/store_fake.go b/pkg/services/folder/folderimpl/store_fake.go index f355bd17262..33af4ebed09 100644 --- a/pkg/services/folder/folderimpl/store_fake.go +++ b/pkg/services/folder/folderimpl/store_fake.go @@ -28,7 +28,7 @@ func (f *fakeStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) return f.ExpectedFolder, f.ExpectedError } -func (f *fakeStore) Delete(ctx context.Context, uid string, orgID int64) error { +func (f *fakeStore) Delete(ctx context.Context, UIDs []string, orgID int64) error { f.DeleteCalled = true return f.ExpectedError } @@ -60,3 +60,7 @@ func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64 func (f *fakeStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) { return f.ExpectedFolders, f.ExpectedError } + +func (f *fakeStore) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) { + return f.ExpectedFolders, f.ExpectedError +} diff --git a/pkg/services/folder/registry.go b/pkg/services/folder/registry.go index 7a6ff865b0e..d87b4945761 100644 --- a/pkg/services/folder/registry.go +++ b/pkg/services/folder/registry.go @@ -7,7 +7,7 @@ import ( ) type RegistryService interface { - DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error - CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error) + DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error + CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) Kind() string } diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 32a402b9371..244f925ddeb 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "errors" + "fmt" + "strings" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" @@ -188,25 +190,41 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S // CountInFolder is a handler for retrieving the number of library panels contained // within a given folder and for a specific organisation. -func (lps LibraryPanelService) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) { +func (lps LibraryPanelService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) { + if len(folderUIDs) == 0 { + return 0, nil + } var count int64 return count, lps.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { - folder, err := lps.FolderService.Get(ctx, &folder.GetFolderQuery{UID: &folderUID, OrgID: orgID, SignedInUser: u}) + metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryPanels).Inc() + // the sequential IDs for the respective entries of dashboard and folder tables are different, + // so we need to get the folder ID from the dashboard table + // TODO: In the future, we should consider adding a folder UID column to the library_element table + // and use that instead of the folder ID. + s := fmt.Sprintf(`SELECT COUNT(*) FROM library_element + WHERE org_id = ? AND folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (%s)) AND kind = ?`, strings.Repeat("?,", len(folderUIDs)-1)+"?") + args := make([]interface{}, 0, len(folderUIDs)+2) + args = append(args, orgID, orgID) + for _, folderUID := range folderUIDs { + args = append(args, folderUID) + } + args = append(args, int64(model.PanelElement)) + _, err := sess.SQL(s, args...).Get(&count) if err != nil { return err } - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryPanels).Inc() - // nolint:staticcheck - q := sess.Table("library_element").Where("org_id = ?", u.GetOrgID()). - Where("folder_id = ?", folder.ID).Where("kind = ?", int64(model.PanelElement)) - count, err = q.Count() return err }) } // DeleteInFolder deletes the library panels contained in a given folder. -func (lps LibraryPanelService) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error { - return lps.LibraryElementService.DeleteLibraryElementsInFolder(ctx, user, folderUID) +func (lps LibraryPanelService) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error { + for _, folderUID := range folderUIDs { + if err := lps.LibraryElementService.DeleteLibraryElementsInFolder(ctx, user, folderUID); err != nil { + return err + } + } + return nil } // Kind returns the name of the library panel type of entity. diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 42ed6ec6273..53b6ce6ecda 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -326,14 +326,14 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "It should return the correct count of library panels in a folder", func(t *testing.T, sc scenarioContext) { - count, err := sc.lps.CountInFolder(context.Background(), sc.user.OrgID, sc.folder.UID, sc.user) + count, err := sc.lps.CountInFolders(context.Background(), sc.user.OrgID, []string{sc.folder.UID}, sc.user) require.NoError(t, err) require.Equal(t, int64(1), count) }) scenarioWithLibraryPanel(t, "It should delete library panels in a folder", func(t *testing.T, sc scenarioContext) { - err := sc.lps.DeleteInFolder(context.Background(), sc.user.OrgID, sc.folder.UID, sc.user) + err := sc.lps.DeleteInFolders(context.Background(), sc.user.OrgID, []string{sc.folder.UID}, sc.user) require.NoError(t, err) _, err = sc.elementService.GetElement(sc.ctx, sc.user, diff --git a/pkg/services/ngalert/migration/permissions_test.go b/pkg/services/ngalert/migration/permissions_test.go index 0931433de79..0b33927b60d 100644 --- a/pkg/services/ngalert/migration/permissions_test.go +++ b/pkg/services/ngalert/migration/permissions_test.go @@ -412,10 +412,10 @@ func TestDashAlertPermissionMigration(t *testing.T) { }, }, dashboards: []*dashboards.Dashboard{ - genDashboard(t, 3, "d_1", "", 1), - genDashboard(t, 4, "d_2", "", 1), - genDashboard(t, 5, "d_3", "", 2), - genDashboard(t, 6, "d_4", "", 2), + genDashboard(t, 3, "d_1", "1", 1), + genDashboard(t, 4, "d_2", "1", 1), + genDashboard(t, 5, "d_3", "2", 2), + genDashboard(t, 6, "d_4", "2", 2), }, dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{ "d_1": { diff --git a/pkg/services/ngalert/migration/service_test.go b/pkg/services/ngalert/migration/service_test.go index f2a3a2a2ccc..02edb31b4e4 100644 --- a/pkg/services/ngalert/migration/service_test.go +++ b/pkg/services/ngalert/migration/service_test.go @@ -1398,6 +1398,7 @@ func TestCommonServicePatterns(t *testing.T) { d1Copy := *d1 //nolint:staticcheck d1Copy.FolderID = f2.ID + d1Copy.FolderUID = f2.UID _, err := x.ID(d1.ID).Update(d1Copy) return err }, diff --git a/pkg/services/ngalert/migration/silences_test.go b/pkg/services/ngalert/migration/silences_test.go index 54f04571980..92a5de4101e 100644 --- a/pkg/services/ngalert/migration/silences_test.go +++ b/pkg/services/ngalert/migration/silences_test.go @@ -33,7 +33,7 @@ func TestSilences(t *testing.T) { o := createOrg(t, 1) folder1 := createFolder(t, 1, o.ID, "folder-1") - dash1 := createDashboard(t, 3, o.ID, "dash1", "", folder1.ID, nil) + dash1 := createDashboard(t, 3, o.ID, "dash1", folder1.UID, folder1.ID, nil) silenceTests := []struct { name string diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 6d3a2e99e97..140f3e3c025 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -318,11 +318,18 @@ func newTitlesOverlapExisting(rules []ngmodels.UpdateRule) bool { // CountInFolder is a handler for retrieving the number of alert rules of // specific organisation associated with a given namespace (parent folder). -func (st DBstore) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) { +func (st DBstore) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) { + if len(folderUIDs) == 0 { + return 0, nil + } var count int64 var err error err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { - q := sess.Table("alert_rule").Where("org_id = ?", orgID).Where("namespace_uid = ?", folderUID) + args := make([]any, 0, len(folderUIDs)) + for _, folderUID := range folderUIDs { + args = append(args, folderUID) + } + q := sess.Table("alert_rule").Where("org_id = ?", orgID).Where(fmt.Sprintf("namespace_uid IN (%s)", strings.Repeat("?,", len(folderUIDs)-1)+"?"), args...) count, err = q.Count() return err }) @@ -577,35 +584,37 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel } // DeleteInFolder deletes the rules contained in a given folder along with their associated data. -func (st DBstore) DeleteInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) error { - evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)) - canSave, err := st.AccessControl.Evaluate(ctx, user, evaluator) - if err != nil { - st.Logger.Error("Failed to evaluate access control", "error", err) - return err - } - if !canSave { - st.Logger.Error("user is not allowed to delete alert rules in folder", "folder", folderUID, "user") - return dashboards.ErrFolderAccessDenied - } - - rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{ - OrgID: orgID, - NamespaceUIDs: []string{folderUID}, - }) - if err != nil { - return err - } - - uids := make([]string, 0, len(rules)) - for _, tgt := range rules { - if tgt != nil { - uids = append(uids, tgt.UID) +func (st DBstore) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) error { + for _, folderUID := range folderUIDs { + evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)) + canSave, err := st.AccessControl.Evaluate(ctx, user, evaluator) + if err != nil { + st.Logger.Error("Failed to evaluate access control", "error", err) + return err + } + if !canSave { + st.Logger.Error("user is not allowed to delete alert rules in folder", "folder", folderUID, "user") + return dashboards.ErrFolderAccessDenied } - } - if err := st.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil { - return err + rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{ + OrgID: orgID, + NamespaceUIDs: []string{folderUID}, + }) + if err != nil { + return err + } + + uids := make([]string, 0, len(rules)) + for _, tgt := range rules { + if tgt != nil { + uids = append(uids, tgt.UID) + } + } + + if err := st.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil { + return err + } } return nil } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index ccbc18c9c6a..c653d32ae83 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -458,8 +458,8 @@ func TestIntegration_CountAlertRules(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - count, err := store.CountInFolder(context.Background(), - test.query.OrgID, test.query.NamespaceUID, nil) + count, err := store.CountInFolders(context.Background(), + test.query.OrgID, []string{test.query.NamespaceUID}, nil) if test.expectErr { require.Error(t, err) } else { @@ -486,7 +486,7 @@ func TestIntegration_DeleteInFolder(t *testing.T) { t.Run("should not be able to delete folder without permissions to delete rules", func(t *testing.T) { store.AccessControl = acmock.New() - err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{}) + err := store.DeleteInFolders(context.Background(), rule.OrgID, []string{rule.NamespaceUID}, &user.SignedInUser{}) require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied) }) @@ -494,10 +494,10 @@ func TestIntegration_DeleteInFolder(t *testing.T) { store.AccessControl = acmock.New().WithPermissions([]accesscontrol.Permission{ {Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll}, }) - err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{}) + err := store.DeleteInFolders(context.Background(), rule.OrgID, []string{rule.NamespaceUID}, &user.SignedInUser{}) require.NoError(t, err) - c, err := store.CountInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, &user.SignedInUser{}) + c, err := store.CountInFolders(context.Background(), rule.OrgID, []string{rule.NamespaceUID}, &user.SignedInUser{}) require.NoError(t, err) require.Equal(t, int64(0), c) }) diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 00df1bb8ad6..c87104c0da7 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -336,6 +336,6 @@ func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, org return result, nil } -func (f *RuleStore) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) { +func (f *RuleStore) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) { return 0, nil }