diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 52b90ef8375..0190b48da68 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -27,7 +27,7 @@ type DashboardService interface { SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error) UpdateDashboardACL(ctx context.Context, uid int64, items []*DashboardACL) error DeleteACLByUser(ctx context.Context, userID int64) error - CountInFolder(ctx context.Context, orgID int64, uid string, user *user.SignedInUser) (int64, error) + CountInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) (int64, error) } // PluginService is a service for operating on plugin dashboards. diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index dcaa62bcab1..139201b6c5c 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -626,8 +626,8 @@ func (dr *DashboardServiceImpl) DeleteACLByUser(ctx context.Context, userID int6 return dr.dashboardStore.DeleteACLByUser(ctx, userID) } -func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, uid string, u *user.SignedInUser) (int64, error) { - folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{UID: &uid, OrgID: orgID, SignedInUser: u}) +func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, folderUID string, u *user.SignedInUser) (int64, error) { + folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{UID: &folderUID, OrgID: orgID, SignedInUser: u}) if err != nil { return 0, err } @@ -635,8 +635,8 @@ func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, u return dr.dashboardStore.CountDashboardsInFolder(ctx, &dashboards.CountDashboardsInFolderRequest{FolderID: folder.ID, OrgID: orgID}) } -func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, UID string) error { - return dr.dashboardStore.DeleteDashboardsInFolder(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUID: UID, OrgID: orgID}) +func (dr *DashboardServiceImpl) DeleteInFolder(ctx context.Context, orgID int64, folderUID string) error { + return dr.dashboardStore.DeleteDashboardsInFolder(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUID: folderUID, OrgID: orgID}) } func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index b5c1740e789..2bac094ee1b 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -484,8 +484,10 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e return dashboards.ErrFolderAccessDenied } - if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID); err != nil { - return err + if cmd.ForceDeleteRules { + if err := s.deleteChildrenInFolder(ctx, dashFolder.OrgID, dashFolder.UID); err != nil { + return err + } } err = s.legacyDelete(ctx, cmd, dashFolder) @@ -499,9 +501,9 @@ func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) e return err } -func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, UID string) error { +func (s *Service) deleteChildrenInFolder(ctx context.Context, orgID int64, folderUID string) error { for _, v := range s.registry { - if err := v.DeleteInFolder(ctx, orgID, UID); err != nil { + if err := v.DeleteInFolder(ctx, orgID, folderUID); err != nil { return err } } @@ -886,11 +888,6 @@ func (s *Service) RegisterService(r folder.RegistryService) error { s.mutex.Lock() defer s.mutex.Unlock() - _, ok := s.registry[r.Kind()] - if ok { - return folder.ErrTargetRegistrySrvConflict.Errorf("target registry service: %s already exists", r.Kind()) - } - s.registry[r.Kind()] = r return nil diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 73818503c0e..ecbd4f0fa5f 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -6,6 +6,7 @@ import ( "fmt" "math/rand" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -27,6 +28,8 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/ngalert/models" + ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -336,6 +339,9 @@ func TestIntegrationNestedFolderService(t *testing.T) { require.NoError(t, err) nestedFolderStore := ProvideStore(db, db.Cfg, featuresFlagOn) + b := bus.ProvideBus(tracing.InitializeTracerForTest()) + ac := acimpl.ProvideAccessControl(cfg) + serviceWithFlagOn := &Service{ cfg: cfg, log: log.New("test-folder-service"), @@ -343,9 +349,9 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOn, - bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + bus: b, db: db, - accessControl: acimpl.ProvideAccessControl(cfg), + accessControl: ac, registry: make(map[string]folder.RegistryService), } @@ -358,16 +364,19 @@ func TestIntegrationNestedFolderService(t *testing.T) { SignedInUser: &signedInUser, } + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() + t.Run("Should get descendant counts", func(t *testing.T) { - ac := acmock.New() - folderPermissions := acmock.NewMockedPermissionsService() - dashboardPermissions := acmock.NewMockedPermissionsService() depth := 5 t.Run("With nested folder feature flag on", func(t *testing.T) { origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - _, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) + dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) + require.NoError(t, err) + + alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv) require.NoError(t, err) ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOn", createCmd) @@ -378,6 +387,8 @@ func TestIntegrationNestedFolderService(t *testing.T) { require.NoError(t, err) _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, "prod") _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, "prod") + _ = createRule(t, alertStore, parent.UID, "parent alert") + _ = createRule(t, alertStore, subfolder.UID, "sub alert") countCmd := folder.GetDescendantCountsQuery{ UID: &ancestorUIDs[0], @@ -386,8 +397,9 @@ func TestIntegrationNestedFolderService(t *testing.T) { } m, err := serviceWithFlagOn.GetDescendantCounts(context.Background(), &countCmd) require.NoError(t, err) - require.Equal(t, m["folder"], int64(depth-1)) - require.Equal(t, m["dashboard"], int64(2)) + require.Equal(t, int64(depth-1), m["folder"]) + require.Equal(t, int64(2), m["dashboard"]) + require.Equal(t, int64(2), m["alertrule"]) t.Cleanup(func() { guardian.New = origNewGuardian @@ -410,7 +422,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, - bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + bus: b, db: db, registry: make(map[string]folder.RegistryService), } @@ -418,7 +430,11 @@ func TestIntegrationNestedFolderService(t *testing.T) { origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - _, err = service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, folderPermissions, dashboardPermissions, ac, serviceWithFlagOff) + dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, + folderPermissions, dashboardPermissions, ac, serviceWithFlagOff) + require.NoError(t, err) + + alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, ac, dashSrv) require.NoError(t, err) ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, depth, "getDescendantCountsOff", createCmd) @@ -429,6 +445,8 @@ func TestIntegrationNestedFolderService(t *testing.T) { require.NoError(t, err) _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in parent", orgID, parent.ID, "prod") _ = insertTestDashboard(t, serviceWithFlagOn.dashboardStore, "dashboard in subfolder", orgID, subfolder.ID, "prod") + _ = createRule(t, alertStore, parent.UID, "parent alert") + _ = createRule(t, alertStore, subfolder.UID, "sub alert") countCmd := folder.GetDescendantCountsQuery{ UID: &ancestorUIDs[0], @@ -437,8 +455,9 @@ func TestIntegrationNestedFolderService(t *testing.T) { } m, err := serviceWithFlagOff.GetDescendantCounts(context.Background(), &countCmd) require.NoError(t, err) - require.Equal(t, m["folder"], int64(0)) - require.Equal(t, m["dashboard"], int64(1)) + require.Equal(t, int64(0), m["folder"]) + require.Equal(t, int64(1), m["dashboard"]) + require.Equal(t, int64(1), m["alertrule"]) t.Cleanup(func() { guardian.New = origNewGuardian @@ -452,29 +471,78 @@ func TestIntegrationNestedFolderService(t *testing.T) { t.Run("Should delete folders", func(t *testing.T) { t.Run("With nested folder feature flag on", func(t *testing.T) { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - - ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "", createCmd) - - deleteCmd := folder.DeleteFolderCommand{ - UID: ancestorUIDs[0], - OrgID: orgID, - SignedInUser: &signedInUser, - } - err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd) + dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) require.NoError(t, err) - for i, uid := range ancestorUIDs { - // dashboard table - _, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) - require.ErrorIs(t, err, dashboards.ErrFolderNotFound) - // folder table - _, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) - require.ErrorIs(t, err, folder.ErrFolderNotFound) - } - t.Cleanup(func() { - guardian.New = origNewGuardian + alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, ac, dashSrv) + require.NoError(t, err) + t.Run("With force deletion of rules", func(t *testing.T) { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "with-force", createCmd) + + parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) + require.NoError(t, err) + subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) + require.NoError(t, err) + _ = createRule(t, alertStore, parent.UID, "parent alert") + _ = createRule(t, alertStore, subfolder.UID, "sub alert") + + deleteCmd := folder.DeleteFolderCommand{ + UID: ancestorUIDs[0], + OrgID: orgID, + SignedInUser: &signedInUser, + ForceDeleteRules: true, + } + err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd) + require.NoError(t, err) + + for i, uid := range ancestorUIDs { + // dashboard table + _, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) + require.ErrorIs(t, err, dashboards.ErrFolderNotFound) + // folder table + _, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) + require.ErrorIs(t, err, folder.ErrFolderNotFound) + } + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + }) + t.Run("Without force deletion of rules", func(t *testing.T) { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 3, "without-force", createCmd) + + parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) + require.NoError(t, err) + subfolder, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[1]) + require.NoError(t, err) + _ = createRule(t, alertStore, parent.UID, "parent alert") + _ = createRule(t, alertStore, subfolder.UID, "sub alert") + + deleteCmd := folder.DeleteFolderCommand{ + UID: ancestorUIDs[0], + OrgID: orgID, + SignedInUser: &signedInUser, + ForceDeleteRules: false, + } + err = serviceWithFlagOn.Delete(context.Background(), &deleteCmd) + require.Error(t, dashboards.ErrFolderContainsAlertRules, err) + + for i, uid := range ancestorUIDs { + // dashboard table + _, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) + require.NoError(t, err) + // folder table + _, err = serviceWithFlagOn.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) + require.NoError(t, err) + } + t.Cleanup(func() { + guardian.New = origNewGuardian + }) }) }) t.Run("With nested folder feature flag off", func(t *testing.T) { @@ -483,6 +551,11 @@ func TestIntegrationNestedFolderService(t *testing.T) { require.NoError(t, err) nestedFolderStore := ProvideStore(db, db.Cfg, featuresFlagOff) + dashSrv, err := service.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, nil, featuresFlagOff, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn) + require.NoError(t, err) + alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOn, ac, dashSrv) + require.NoError(t, err) + serviceWithFlagOff := &Service{ cfg: cfg, log: log.New("test-folder-service"), @@ -490,37 +563,74 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, - bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + bus: b, db: db, } + t.Run("With force deletion of rules", func(t *testing.T) { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "off-force", createCmd) - ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "", createCmd) + parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) + require.NoError(t, err) + _ = createRule(t, alertStore, parent.UID, "parent alert") - deleteCmd := folder.DeleteFolderCommand{ - UID: ancestorUIDs[0], - OrgID: orgID, - SignedInUser: &signedInUser, - } - err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd) - require.NoError(t, err) + deleteCmd := folder.DeleteFolderCommand{ + UID: ancestorUIDs[0], + OrgID: orgID, + SignedInUser: &signedInUser, + ForceDeleteRules: true, + } + err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd) + require.NoError(t, err) - for i, uid := range ancestorUIDs { // dashboard table - _, err := serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, uid) + _, err = serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) // folder table - _, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[i], OrgID: orgID}) + _, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[0], OrgID: orgID}) require.NoError(t, err) - } - t.Cleanup(func() { - guardian.New = origNewGuardian - for _, uid := range ancestorUIDs { - err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID) - require.NoError(t, err) + t.Cleanup(func() { + guardian.New = origNewGuardian + for _, uid := range ancestorUIDs { + err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID) + require.NoError(t, err) + } + }) + }) + t.Run("Without force deletion of rules", func(t *testing.T) { + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true}) + + ancestorUIDs := CreateSubtreeInStore(t, nestedFolderStore, serviceWithFlagOn, 1, "off-no-force", createCmd) + + parent, err := serviceWithFlagOn.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) + require.NoError(t, err) + _ = createRule(t, alertStore, parent.UID, "parent alert") + + deleteCmd := folder.DeleteFolderCommand{ + UID: ancestorUIDs[0], + OrgID: orgID, + SignedInUser: &signedInUser, + ForceDeleteRules: false, } + err = serviceWithFlagOff.Delete(context.Background(), &deleteCmd) + require.Error(t, dashboards.ErrFolderContainsAlertRules, err) + + // dashboard table + _, err = serviceWithFlagOff.dashboardFolderStore.GetFolderByUID(context.Background(), orgID, ancestorUIDs[0]) + require.NoError(t, err) + // folder table + _, err = serviceWithFlagOff.store.Get(context.Background(), folder.GetFolderQuery{UID: &ancestorUIDs[0], OrgID: orgID}) + require.NoError(t, err) + t.Cleanup(func() { + guardian.New = origNewGuardian + for _, uid := range ancestorUIDs { + err := serviceWithFlagOff.store.Delete(context.Background(), uid, orgID) + require.NoError(t, err) + } + }) }) }) }) @@ -1076,3 +1186,25 @@ func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder db: db, } } + +func createRule(t *testing.T, store *ngstore.DBstore, folderUID, title string) *models.AlertRule { + t.Helper() + + rule := models.AlertRule{ + OrgID: orgID, + NamespaceUID: folderUID, + Title: title, + Updated: time.Now(), + UID: util.GenerateShortUID(), + } + err := store.SQLStore.WithDbSession(context.Background(), func(sess *db.Session) error { + _, err := sess.Table(models.AlertRule{}).InsertOne(rule) + if err != nil { + return err + } + return nil + }) + require.NoError(t, err) + + return &rule +} diff --git a/pkg/services/folder/registry.go b/pkg/services/folder/registry.go index 0692957b9fd..468f097bf8c 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, uid string) error - CountInFolder(ctx context.Context, orgID int64, uid string, user *user.SignedInUser) (int64, error) + DeleteInFolder(ctx context.Context, orgID int64, folderUID string) error + CountInFolder(ctx context.Context, orgID int64, folderUID string, user *user.SignedInUser) (int64, error) Kind() string } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 7e4477bdb3d..e35205a3f17 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -67,6 +67,7 @@ func ProvideService( annotationsRepo annotations.Repository, pluginsStore plugins.Store, tracer tracing.Tracer, + ruleStore *store.DBstore, ) (*AlertNG, error) { ng := &AlertNG{ Cfg: cfg, @@ -92,6 +93,7 @@ func ProvideService( annotationsRepo: annotationsRepo, pluginsStore: pluginsStore, tracer: tracer, + store: ruleStore, } if ng.IsDisabled() { @@ -149,25 +151,16 @@ func (ng *AlertNG) init() error { initCtx, cancelFunc := context.WithTimeout(context.Background(), 30*time.Second) defer cancelFunc() - store := &store.DBstore{ - Cfg: ng.Cfg.UnifiedAlerting, - FeatureToggles: ng.FeatureToggles, - SQLStore: ng.SQLStore, - Logger: ng.Log, - FolderService: ng.folderService, - AccessControl: ng.accesscontrol, - DashboardService: ng.dashboardService, - } - ng.store = store + ng.store.Logger = ng.Log decryptFn := ng.SecretsService.GetDecryptedValue multiOrgMetrics := ng.Metrics.GetMultiOrgAlertmanagerMetrics() - ng.MultiOrgAlertmanager, err = notifier.NewMultiOrgAlertmanager(ng.Cfg, store, store, ng.KVStore, store, decryptFn, multiOrgMetrics, ng.NotificationService, log.New("ngalert.multiorg.alertmanager"), ng.SecretsService) + ng.MultiOrgAlertmanager, err = notifier.NewMultiOrgAlertmanager(ng.Cfg, ng.store, ng.store, ng.KVStore, ng.store, decryptFn, multiOrgMetrics, ng.NotificationService, log.New("ngalert.multiorg.alertmanager"), ng.SecretsService) if err != nil { return err } - imageService, err := image.NewScreenshotImageServiceFromCfg(ng.Cfg, store, ng.dashboardService, ng.renderService, ng.Metrics.Registerer) + imageService, err := image.NewScreenshotImageServiceFromCfg(ng.Cfg, ng.store, ng.dashboardService, ng.renderService, ng.Metrics.Registerer) if err != nil { return err } @@ -186,7 +179,7 @@ func (ng *AlertNG) init() error { clk := clock.New() - alertsRouter := sender.NewAlertsRouter(ng.MultiOrgAlertmanager, store, clk, appUrl, ng.Cfg.UnifiedAlerting.DisabledOrgs, + alertsRouter := sender.NewAlertsRouter(ng.MultiOrgAlertmanager, ng.store, clk, appUrl, ng.Cfg.UnifiedAlerting.DisabledOrgs, ng.Cfg.UnifiedAlerting.AdminConfigPollInterval, ng.DataSourceService, ng.SecretsService) // Make sure we sync at least once as Grafana starts to get the router up and running before we start sending any alerts. @@ -205,7 +198,7 @@ func (ng *AlertNG) init() error { DisableGrafanaFolder: ng.Cfg.UnifiedAlerting.ReservedLabels.IsReservedLabelDisabled(models.FolderTitleLabel), AppURL: appUrl, EvaluatorFactory: evalFactory, - RuleStore: store, + RuleStore: ng.store, Metrics: ng.Metrics.GetSchedulerMetrics(), AlertSender: alertsRouter, Tracer: ng.tracer, @@ -221,7 +214,7 @@ func (ng *AlertNG) init() error { cfg := state.ManagerCfg{ Metrics: ng.Metrics.GetStateMetrics(), ExternalURL: appUrl, - InstanceStore: store, + InstanceStore: ng.store, Images: ng.imageService, Clock: clk, Historian: history, @@ -232,18 +225,18 @@ func (ng *AlertNG) init() error { // if it is required to include folder title to the alerts, we need to subscribe to changes of alert title if !ng.Cfg.UnifiedAlerting.ReservedLabels.IsReservedLabelDisabled(models.FolderTitleLabel) { - subscribeToFolderChanges(ng.Log, ng.bus, store) + subscribeToFolderChanges(ng.Log, ng.bus, ng.store) } ng.stateManager = stateManager ng.schedule = scheduler // Provisioning - policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Cfg.UnifiedAlerting, ng.Log) - contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) - templateService := provisioning.NewTemplateService(store, store, store, ng.Log) - muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) - alertRuleService := provisioning.NewAlertRuleService(store, store, ng.dashboardService, ng.QuotaService, store, + policyService := provisioning.NewNotificationPolicyService(ng.store, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log) + contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, ng.Log) + templateService := provisioning.NewTemplateService(ng.store, ng.store, ng.store, ng.Log) + muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log) + alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.dashboardService, ng.QuotaService, ng.store, int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()), ng.Log) @@ -254,11 +247,11 @@ func (ng *AlertNG) init() error { RouteRegister: ng.RouteRegister, DataProxy: ng.DataProxy, QuotaService: ng.QuotaService, - TransactionManager: store, - RuleStore: store, - AlertingStore: store, - AdminConfigStore: store, - ProvenanceStore: store, + TransactionManager: ng.store, + RuleStore: ng.store, + AlertingStore: ng.store, + AdminConfigStore: ng.store, + ProvenanceStore: ng.store, MultiOrgAlertmanager: ng.MultiOrgAlertmanager, StateManager: ng.stateManager, AccessControl: ng.accesscontrol, diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 893181ff697..c3ac5fd3480 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -19,16 +19,15 @@ import ( func TestAlertRuleService(t *testing.T) { ruleService := createAlertRuleService(t) + var orgID int64 = 1 t.Run("alert rule creation should return the created id", func(t *testing.T) { - var orgID int64 = 1 rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#1", orgID), models.ProvenanceNone, 0) require.NoError(t, err) require.NotEqual(t, 0, rule.ID, "expected to get the created id and not the zero value") }) t.Run("alert rule creation should set the right provenance", func(t *testing.T) { - var orgID int64 = 1 rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#2", orgID), models.ProvenanceAPI, 0) require.NoError(t, err) @@ -38,7 +37,6 @@ func TestAlertRuleService(t *testing.T) { }) t.Run("group creation should set the right provenance", func(t *testing.T) { - var orgID int64 = 1 group := createDummyGroup("group-test-1", orgID) err := ruleService.ReplaceRuleGroup(context.Background(), orgID, group, 0, models.ProvenanceAPI) require.NoError(t, err) @@ -54,7 +52,6 @@ func TestAlertRuleService(t *testing.T) { }) t.Run("alert rule group should be updated correctly", func(t *testing.T) { - var orgID int64 = 1 rule := dummyRule("test#3", orgID) rule.RuleGroup = "a" rule, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone, 0) @@ -84,7 +81,6 @@ func TestAlertRuleService(t *testing.T) { }) t.Run("group creation should propagate group title correctly", func(t *testing.T) { - var orgID int64 = 1 group := createDummyGroup("group-test-3", orgID) group.Rules[0].RuleGroup = "something different" @@ -100,7 +96,6 @@ func TestAlertRuleService(t *testing.T) { }) t.Run("alert rule should get interval from existing rule group", func(t *testing.T) { - var orgID int64 = 1 rule := dummyRule("test#4", orgID) rule.RuleGroup = "b" rule, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone, 0) @@ -147,7 +142,6 @@ func TestAlertRuleService(t *testing.T) { }) t.Run("updating a group by updating a rule should bump that rule's data and version number", func(t *testing.T) { - var orgID int64 = 1 group := createDummyGroup("group-test-5", orgID) err := ruleService.ReplaceRuleGroup(context.Background(), orgID, group, 0, models.ProvenanceAPI) require.NoError(t, err) @@ -167,7 +161,6 @@ func TestAlertRuleService(t *testing.T) { }) t.Run("updating a group by updating a rule should not remove dashboard and panel ids", func(t *testing.T) { - var orgID int64 = 1 dashboardUid := "huYnkl7H" panelId := int64(5678) group := createDummyGroup("group-test-5", orgID) @@ -316,7 +309,7 @@ func TestAlertRuleService(t *testing.T) { checker.EXPECT().LimitExceeded() ruleService.quotas = checker - _, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#1", 1), models.ProvenanceNone, 0) + _, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#1", orgID), models.ProvenanceNone, 0) require.ErrorIs(t, err, models.ErrQuotaReached) }) @@ -358,10 +351,10 @@ func createAlertRuleService(t *testing.T) AlertRuleService { } func dummyRule(title string, orgID int64) models.AlertRule { - return createTestRule(title, "my-cool-group", orgID) + return createTestRule(title, "my-cool-group", orgID, "my-namespace") } -func createTestRule(title string, groupTitle string, orgID int64) models.AlertRule { +func createTestRule(title string, groupTitle string, orgID int64, namespace string) models.AlertRule { return models.AlertRule{ OrgID: orgID, Title: title, @@ -379,7 +372,7 @@ func createTestRule(title string, groupTitle string, orgID int64) models.AlertRu }, }, }, - NamespaceUID: "my-namespace", + NamespaceUID: namespace, RuleGroup: groupTitle, For: time.Second * 60, NoDataState: models.OK, diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 287f2046501..7e6f080aa42 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" + "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) @@ -230,13 +231,13 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []ngmodels.UpdateR }) } -// CountAlertRulesInFolder is a handler for retrieving the number of alert rules of +// CountInFolder is a handler for retrieving the number of alert rules of // specific organisation associated with a given namespace (parent folder). -func (st DBstore) CountAlertRulesInFolder(ctx context.Context, query *ngmodels.CountAlertRulesQuery) (int64, error) { +func (st DBstore) CountInFolder(ctx context.Context, orgID int64, folderUID string, u *user.SignedInUser) (int64, error) { var count int64 var err error err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { - q := sess.Table("alert_rule").Where("org_id = ?", query.OrgID).Where("namespace_uid = ?", query.NamespaceUID) + q := sess.Table("alert_rule").Where("org_id = ?", orgID).Where("namespace_uid = ?", folderUID) count, err = q.Count() return err }) @@ -494,6 +495,32 @@ 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) error { + 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 +} + +// Kind returns the name of the alert rule type of entity. +func (st DBstore) Kind() string { return entity.StandardKindAlertRule } + // GenerateNewAlertRuleUID generates a unique UID for a rule. // This is set as a variable so that the tests can override it. // The ruleTitle is only used by the mocked functions. diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 17b4d343a70..87e77bf731e 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -8,9 +8,11 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/ngalert/testutil" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" @@ -209,7 +211,8 @@ func TestIntegration_CountAlertRules(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - count, err := store.CountAlertRulesInFolder(context.Background(), test.query) + count, err := store.CountInFolder(context.Background(), + test.query.OrgID, test.query.NamespaceUID, nil) if test.expectErr { require.Error(t, err) } else { @@ -220,6 +223,28 @@ func TestIntegration_CountAlertRules(t *testing.T) { } } +func TestIntegration_DeleteInFolder(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + sqlStore := db.InitTestDB(t) + cfg := setting.NewCfg() + store := &DBstore{ + SQLStore: sqlStore, + FolderService: setupFolderService(t, sqlStore, cfg), + Logger: log.New("test-dbstore"), + } + rule := createRule(t, store, nil) + + err := store.DeleteInFolder(context.Background(), rule.OrgID, rule.NamespaceUID) + require.NoError(t, err) + + c, err := store.CountInFolder(context.Background(), rule.OrgID, rule.NamespaceUID, nil) + require.NoError(t, err) + require.Equal(t, int64(0), c) +} + func createRule(t *testing.T, store *DBstore, generate func() *models.AlertRule) *models.AlertRule { t.Helper() if generate == nil { @@ -275,7 +300,7 @@ func setupFolderService(t *testing.T, sqlStore *sqlstore.SQLStore, cfg *setting. tracer := tracing.InitializeTracerForTest() inProcBus := bus.ProvideBus(tracer) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - _, dashboardStore := SetupDashboardService(t, sqlStore, folderStore, cfg) + _, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg) - return SetupFolderService(t, cfg, dashboardStore, folderStore, inProcBus) + return testutil.SetupFolderService(t, cfg, dashboardStore, folderStore, inProcBus) } diff --git a/pkg/services/ngalert/store/database.go b/pkg/services/ngalert/store/database.go index 16a170ae23e..faf3f09f287 100644 --- a/pkg/services/ngalert/store/database.go +++ b/pkg/services/ngalert/store/database.go @@ -45,14 +45,18 @@ type DBstore struct { func ProvideDBStore( cfg *setting.Cfg, featureToggles featuremgmt.FeatureToggles, sqlstore db.DB, folderService folder.Service, - access accesscontrol.AccessControl, dashboards dashboards.DashboardService) *DBstore { - return &DBstore{ + access accesscontrol.AccessControl, dashboards dashboards.DashboardService) (*DBstore, error) { + store := DBstore{ Cfg: cfg.UnifiedAlerting, FeatureToggles: featureToggles, SQLStore: sqlstore, - Logger: log.New("dbstore"), + Logger: log.New("ngalert.dbstore"), FolderService: folderService, AccessControl: access, DashboardService: dashboards, } + if err := folderService.RegisterService(store); err != nil { + return nil, err + } + return &store, nil } diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index 6d28dd7005e..3cd4ba52e75 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -6,25 +6,7 @@ import ( "sync" "testing" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/services/accesscontrol" - acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/dashboards/database" - dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/folder/foldertest" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/tag/tagimpl" - "github.com/grafana/grafana/pkg/setting" ) func NewFakeImageStore(t *testing.T) *FakeImageStore { @@ -130,46 +112,3 @@ func (f *FakeAdminConfigStore) UpdateAdminConfiguration(cmd UpdateAdminConfigura return nil } - -func SetupFolderService(tb testing.TB, cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus) folder.Service { - tb.Helper() - - ac := acmock.New() - features := featuremgmt.WithFeatures() - - return folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, nil, features) -} - -func SetupDashboardService(tb testing.TB, sqlStore *sqlstore.SQLStore, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { - tb.Helper() - - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanSaveValue: true, - CanViewValue: true, - CanAdminValue: true, - }) - tb.Cleanup(func() { - guardian.New = origNewGuardian - }) - - ac := acmock.New() - dashboardPermissions := acmock.NewMockedPermissionsService() - folderPermissions := acmock.NewMockedPermissionsService() - folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - - features := featuremgmt.WithFeatures() - quotaService := quotatest.New(false, nil) - - dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) - require.NoError(tb, err) - - dashboardService, err := dashboardservice.ProvideDashboardServiceImpl( - cfg, dashboardStore, fs, nil, - features, folderPermissions, dashboardPermissions, ac, - foldertest.NewFakeService(), - ) - require.NoError(tb, err) - - return dashboardService, dashboardStore -} diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 00a60380c92..43b4fa19061 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -347,3 +347,7 @@ func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, org func (f *RuleStore) Count(ctx context.Context, orgID int64) (int64, error) { return 0, nil } + +func (f *RuleStore) CountInFolder(ctx context.Context, orgID int64, folderUID string, u *user.SignedInUser) (int64, error) { + return 0, nil +} diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index f04dcf9d408..8fba94f8785 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/ngalert/testutil" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/secrets/database" @@ -59,12 +60,15 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, tracer := tracing.InitializeTracerForTest() bus := bus.ProvideBus(tracer) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - dashboardService, dashboardStore := store.SetupDashboardService(tb, sqlStore, folderStore, cfg) - folderService := store.SetupFolderService(tb, cfg, dashboardStore, folderStore, bus) - + dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg) + folderService := testutil.SetupFolderService(tb, cfg, dashboardStore, folderStore, bus) + ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService, + ac, &dashboards.FakeDashboardService{}) + require.NoError(tb, err) ng, err := ngalert.ProvideService( cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil), - secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, annotationstest.NewFakeAnnotationsRepo(), &plugins.FakePluginStore{}, tracer, + secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, + annotationstest.NewFakeAnnotationsRepo(), &plugins.FakePluginStore{}, tracer, ruleStore, ) require.NoError(tb, err) return ng, &store.DBstore{ diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go new file mode 100644 index 00000000000..887930ce8c6 --- /dev/null +++ b/pkg/services/ngalert/testutil/testutil.go @@ -0,0 +1,66 @@ +package testutil + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/services/accesscontrol" + acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/tag/tagimpl" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func SetupFolderService(tb testing.TB, cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus) folder.Service { + tb.Helper() + + ac := acmock.New() + features := featuremgmt.WithFeatures() + + return folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, nil, features) +} + +func SetupDashboardService(tb testing.TB, sqlStore *sqlstore.SQLStore, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { + tb.Helper() + + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanSaveValue: true, + CanViewValue: true, + CanAdminValue: true, + }) + tb.Cleanup(func() { + guardian.New = origNewGuardian + }) + + ac := acmock.New() + dashboardPermissions := acmock.NewMockedPermissionsService() + folderPermissions := acmock.NewMockedPermissionsService() + folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + + features := featuremgmt.WithFeatures() + quotaService := quotatest.New(false, nil) + + dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) + require.NoError(tb, err) + + dashboardService, err := dashboardservice.ProvideDashboardServiceImpl( + cfg, dashboardStore, fs, nil, + features, folderPermissions, dashboardPermissions, ac, + foldertest.NewFakeService(), + ) + require.NoError(tb, err) + + return dashboardService, dashboardStore +} diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index e9e289508b0..61860a74b44 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota" @@ -477,9 +478,13 @@ func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService _, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, sqlStore.Cfg, featuremgmt.WithFeatures(), acmock.New().WithDisabled(), acmock.NewMockedPermissionsService(), quotaService) require.NoError(t, err) m := metrics.NewNGAlert(prometheus.NewRegistry()) + ruleStore, err := ngstore.ProvideDBStore(sqlStore.Cfg, featuremgmt.WithFeatures(), sqlStore, &foldertest.FakeService{}, + &acmock.Mock{}, &dashboards.FakeDashboardService{}) + require.NoError(t, err) _, err = ngalert.ProvideService( sqlStore.Cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotaService, - secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, annotationstest.NewFakeAnnotationsRepo(), &plugins.FakePluginStore{}, tracer, + secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, + annotationstest.NewFakeAnnotationsRepo(), &plugins.FakePluginStore{}, tracer, ruleStore, ) require.NoError(t, err) _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), sqlStore.Cfg, quotaService, storesrv.ProvideSystemUsersService()) diff --git a/pkg/services/store/entity/models.go b/pkg/services/store/entity/models.go index 84784f2808a..12764ebc40a 100644 --- a/pkg/services/store/entity/models.go +++ b/pkg/services/store/entity/models.go @@ -42,6 +42,10 @@ const ( // the kind may need to change to better encapsulate { targets:[], transforms:[] } StandardKindQuery = "query" + // KindAlertRule is not a real kind. It's used to refer to alert rules, for instance + // in the folder registry service. + StandardKindAlertRule = "alertrule" + //---------------------------------------- // References are referenced from objects //---------------------------------------- diff --git a/public/app/features/browse-dashboards/state/actions.ts b/public/app/features/browse-dashboards/state/actions.ts index c6d3aa8c4e7..3f91914bd56 100644 --- a/public/app/features/browse-dashboards/state/actions.ts +++ b/public/app/features/browse-dashboards/state/actions.ts @@ -19,8 +19,8 @@ export const deleteDashboard = createAsyncThunk('browseDashboards/deleteDashboar export const deleteFolder = createAsyncThunk('browseDashboards/deleteFolder', async (folderUID: string) => { return getBackendSrv().delete(`/api/folders/${folderUID}`, undefined, { - // TODO: Once backend returns alert rule counts, set this back to true - // when this is merged https://github.com/grafana/grafana/pull/67259 + // TODO: Revisit this field when this permissions issue is resolved + // https://github.com/grafana/grafana-enterprise/issues/5144 params: { forceDeleteRules: false }, }); });