Alerting: update rule versions on folder move (#88376)

* Alerting: update rule versions on folder move (#88361)
* Add tracing to folder.Move and folder.Update
This commit is contained in:
Alexander Akhmetov
2024-08-13 12:26:26 +02:00
committed by GitHub
parent 8044cb50f1
commit b2eeb0dd6e
21 changed files with 196 additions and 57 deletions

View File

@@ -834,7 +834,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() dashboardPermissions := accesscontrolmock.NewMockedPermissionsService()
folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
dashboardStore, folderStore, db.InitTestDB(t), features, supportbundlestest.NewFakeBundleService(), nil) dashboardStore, folderStore, db.InitTestDB(t), features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
if dashboardService == nil { if dashboardService == nil {
dashboardService, err = service.ProvideDashboardServiceImpl( dashboardService, err = service.ProvideDashboardServiceImpl(

View File

@@ -458,7 +458,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
folderStore := folderimpl.ProvideDashboardFolderStore(sc.db.DB()) folderStore := folderimpl.ProvideDashboardFolderStore(sc.db.DB())
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, sc.db.DB(), features, supportbundlestest.NewFakeBundleService(), nil) folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, sc.db.DB(), features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
cfg := setting.NewCfg() cfg := setting.NewCfg()
actionSets := resourcepermissions.NewActionSetService(features) actionSets := resourcepermissions.NewActionSetService(features)

View File

@@ -71,10 +71,13 @@ type DataSourceCreated struct {
OrgID int64 `json:"org_id"` OrgID int64 `json:"org_id"`
} }
type FolderTitleUpdated struct { // FolderFullPathUpdated is emitted when the full path of the folder(s) is updated.
// For example, when the folder is renamed or moved to another folder.
// It does not contain the full path of the folders because calculating
// it requires more resources and not needed in the event at the moment.
type FolderFullPathUpdated struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Title string `json:"name"`
ID int64 `json:"id"` ID int64 `json:"id"`
UID string `json:"uid"` UIDs []string `json:"uids"`
OrgID int64 `json:"org_id"` OrgID int64 `json:"org_id"`
} }

View File

@@ -225,7 +225,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
}) })
ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient())
folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, supportbundlestest.NewFakeBundleService(), nil) folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
cfg.AnnotationMaximumTagsLength = 60 cfg.AnnotationMaximumTagsLength = 60

View File

@@ -303,7 +303,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
guardian.New = origNewGuardian guardian.New = origNewGuardian
}) })
folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore.DB()), sqlStore.DB(), features, supportbundlestest.NewFakeBundleService(), nil) folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore.DB()), sqlStore.DB(), features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
parentUID := "" parentUID := ""
for i := 0; ; i++ { for i := 0; ; i++ {

View File

@@ -830,7 +830,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient())
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
user := &user.SignedInUser{ user := &user.SignedInUser{
OrgID: 1, OrgID: 1,
@@ -948,7 +948,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient())
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
user := &user.SignedInUser{ user := &user.SignedInUser{
OrgID: 1, OrgID: 1,

View File

@@ -13,6 +13,8 @@ import (
"github.com/grafana/dskit/concurrency" "github.com/grafana/dskit/concurrency"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -20,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
@@ -44,12 +47,14 @@ type Service struct {
dashboardFolderStore folder.FolderStore dashboardFolderStore folder.FolderStore
features featuremgmt.FeatureToggles features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl accessControl accesscontrol.AccessControl
// bus is currently used to publish event in case of title change // bus is currently used to publish event in case of folder full path change.
// For example when a folder is moved to another folder or when a folder is renamed.
bus bus.Bus bus bus.Bus
mutex sync.RWMutex mutex sync.RWMutex
registry map[string]folder.RegistryService registry map[string]folder.RegistryService
metrics *foldersMetrics metrics *foldersMetrics
tracer tracing.Tracer
} }
func ProvideService( func ProvideService(
@@ -61,6 +66,7 @@ func ProvideService(
features featuremgmt.FeatureToggles, features featuremgmt.FeatureToggles,
supportBundles supportbundles.Service, supportBundles supportbundles.Service,
r prometheus.Registerer, r prometheus.Registerer,
tracer tracing.Tracer,
) folder.Service { ) folder.Service {
store := ProvideStore(db) store := ProvideStore(db)
srv := &Service{ srv := &Service{
@@ -74,6 +80,7 @@ func ProvideService(
db: db, db: db,
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(r), metrics: newFoldersMetrics(r),
tracer: tracer,
} }
srv.DBMigration(db) srv.DBMigration(db)
@@ -655,6 +662,9 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (
} }
func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) { func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Update")
defer span.End()
if cmd.SignedInUser == nil { if cmd.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user") return nil, folder.ErrBadRequest.Errorf("missing signed in user")
} }
@@ -679,14 +689,8 @@ func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (
if cmd.NewTitle != nil { if cmd.NewTitle != nil {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
if err := s.bus.Publish(ctx, &events.FolderTitleUpdated{
Timestamp: foldr.Updated, if err := s.publishFolderFullPathUpdatedEvent(ctx, foldr.Updated, cmd.OrgID, cmd.UID); err != nil {
Title: foldr.Title,
ID: dashFolder.ID, // nolint:staticcheck
UID: dashFolder.UID,
OrgID: cmd.OrgID,
}); err != nil {
s.log.ErrorContext(ctx, "failed to publish FolderTitleUpdated event", "folder", foldr.Title, "user", cmd.SignedInUser.GetID(), "error", err)
return err return err
} }
} }
@@ -873,6 +877,9 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm
} }
func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) { func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Move")
defer span.End()
if cmd.SignedInUser == nil { if cmd.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user") return nil, folder.ErrBadRequest.Errorf("missing signed in user")
} }
@@ -947,6 +954,10 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol
return folder.ErrInternal.Errorf("failed to move legacy folder: %w", err) return folder.ErrInternal.Errorf("failed to move legacy folder: %w", err)
} }
if err := s.publishFolderFullPathUpdatedEvent(ctx, f.Updated, cmd.OrgID, cmd.UID); err != nil {
return err
}
return nil return nil
}); err != nil { }); err != nil {
return nil, err return nil, err
@@ -954,6 +965,36 @@ func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*fol
return f, nil return f, nil
} }
func (s *Service) publishFolderFullPathUpdatedEvent(ctx context.Context, timestamp time.Time, orgID int64, folderUID string) error {
ctx, span := s.tracer.Start(ctx, "folder.publishFolderFullPathUpdatedEvent")
defer span.End()
descFolders, err := s.store.GetDescendants(ctx, orgID, folderUID)
if err != nil {
s.log.ErrorContext(ctx, "Failed to get descendants of the folder", "folderUID", folderUID, "orgID", orgID, "error", err)
return err
}
uids := make([]string, 0, len(descFolders)+1)
uids = append(uids, folderUID)
for _, f := range descFolders {
uids = append(uids, f.UID)
}
span.AddEvent("found folder descendants", trace.WithAttributes(
attribute.Int64("folders", int64(len(uids))),
))
if err := s.bus.Publish(ctx, &events.FolderFullPathUpdated{
Timestamp: timestamp,
UIDs: uids,
OrgID: orgID,
}); err != nil {
s.log.ErrorContext(ctx, "Failed to publish FolderFullPathUpdated event", "folderUID", folderUID, "orgID", orgID, "descendantsUIDs", uids, "error", err)
return err
}
return nil
}
func (s *Service) canMove(ctx context.Context, cmd *folder.MoveFolderCommand) (bool, error) { func (s *Service) canMove(ctx context.Context, cmd *folder.MoveFolderCommand) (bool, error) {
// Check that the user is allowed to move the folder to the destination folder // Check that the user is allowed to move the folder to the destination folder
var evaluator accesscontrol.Evaluator var evaluator accesscontrol.Evaluator

View File

@@ -63,7 +63,7 @@ func TestIntegrationProvideFolderService(t *testing.T) {
t.Run("should register scope resolvers", func(t *testing.T) { t.Run("should register scope resolvers", func(t *testing.T) {
ac := acmock.New() ac := acmock.New()
db := db.InitTestDB(t) db := db.InitTestDB(t)
ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil) ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3)
}) })
@@ -100,6 +100,7 @@ func TestIntegrationFolderService(t *testing.T) {
accessControl: acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), accessControl: acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
tracer: tracing.InitializeTracerForTest(),
} }
require.NoError(t, service.RegisterService(alertingStore)) require.NoError(t, service.RegisterService(alertingStore))
@@ -440,6 +441,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
accessControl: ac, accessControl: ac,
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
@@ -553,6 +555,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
db: db, db: db,
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
origNewGuardian := guardian.New origNewGuardian := guardian.New
@@ -630,6 +633,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
db: db, db: db,
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
testCases := []struct { testCases := []struct {
@@ -805,6 +809,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) {
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
t.Run("create folder", func(t *testing.T) { t.Run("create folder", func(t *testing.T) {
nestedFolderStore.ExpectedFolder = &folder.Folder{ParentUID: util.GenerateShortUID()} nestedFolderStore.ExpectedFolder = &folder.Folder{ParentUID: util.GenerateShortUID()}
@@ -841,6 +846,7 @@ func TestFolderServiceDualWrite(t *testing.T) {
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
bus: bus.ProvideBus(tracing.InitializeTracerForTest()), bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
} }
@@ -1475,6 +1481,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
accessControl: ac, accessControl: ac,
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
dashboardPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService()
@@ -1977,6 +1984,7 @@ func TestFolderServiceGetFolders(t *testing.T) {
accessControl: ac, accessControl: ac,
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{ signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
@@ -2063,6 +2071,7 @@ func TestGetChildrenFilterByPermission(t *testing.T) {
accessControl: ac, accessControl: ac,
registry: make(map[string]folder.RegistryService), registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
origGuardian := guardian.New origGuardian := guardian.New
@@ -2523,6 +2532,7 @@ func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder
accessControl: ac, accessControl: ac,
db: db, db: db,
metrics: newFoldersMetrics(nil), metrics: newFoldersMetrics(nil),
tracer: tracing.InitializeTracerForTest(),
} }
} }

View File

@@ -329,7 +329,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
require.NoError(t, err) require.NoError(t, err)
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
t.Logf("Creating folder with title and UID %q", title) t.Logf("Creating folder with title and UID %q", title)
ctx := identity.WithRequester(context.Background(), &sc.user) ctx := identity.WithRequester(context.Background(), &sc.user)
folder, err := s.Create(ctx, &folder.CreateFolderCommand{ folder, err := s.Create(ctx, &folder.CreateFolderCommand{
@@ -463,7 +463,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
Cfg: cfg, Cfg: cfg,
features: featuremgmt.WithFeatures(), features: featuremgmt.WithFeatures(),
SQLStore: sqlStore, SQLStore: sqlStore,
folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil), folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()),
} }
// deliberate difference between signed in user and user in db to make it crystal clear // deliberate difference between signed in user and user in db to make it crystal clear

View File

@@ -753,7 +753,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
dashboardStore, err := database.ProvideDashboardStore(sc.replStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService) dashboardStore, err := database.ProvideDashboardStore(sc.replStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService)
require.NoError(t, err) require.NoError(t, err)
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
t.Logf("Creating folder with title and UID %q", title) t.Logf("Creating folder with title and UID %q", title)
ctx := identity.WithRequester(context.Background(), sc.user) ctx := identity.WithRequester(context.Background(), sc.user)
@@ -836,7 +836,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
dashboardStore, err := database.ProvideDashboardStore(replStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) dashboardStore, err := database.ProvideDashboardStore(replStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err) require.NoError(t, err)
features := featuremgmt.WithFeatures() features := featuremgmt.WithFeatures()
folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures(), ac) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures(), ac)
service := LibraryPanelService{ service := LibraryPanelService{

View File

@@ -1819,7 +1819,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
require.NoError(t, err) require.NoError(t, err)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
folderService := folderimpl.ProvideService(actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil) folderService := folderimpl.ProvideService(actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
store := store.DBstore{ store := store.DBstore{
Logger: log, Logger: log,
SQLStore: sqlStore, SQLStore: sqlStore,

View File

@@ -26,8 +26,8 @@ type RuleStore interface {
UpdateAlertRules(ctx context.Context, rule []ngmodels.UpdateRule) error UpdateAlertRules(ctx context.Context, rule []ngmodels.UpdateRule) error
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
// IncreaseVersionForAllRulesInNamespace Increases version for all rules that have specified namespace. Returns all rules that belong to the namespace // IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace uids
IncreaseVersionForAllRulesInNamespace(ctx context.Context, orgID int64, namespaceUID string) ([]ngmodels.AlertRuleKeyWithVersion, error) IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error)
accesscontrol.RuleUIDToNamespaceStore accesscontrol.RuleUIDToNamespaceStore
} }

View File

@@ -487,15 +487,16 @@ func (ng *AlertNG) init() error {
} }
func subscribeToFolderChanges(logger log.Logger, bus bus.Bus, dbStore api.RuleStore) { func subscribeToFolderChanges(logger log.Logger, bus bus.Bus, dbStore api.RuleStore) {
// if folder title is changed, we update all alert rules in that folder to make sure that all peers (in HA mode) will update folder title and // if full path to the folder is changed, we update all alert rules in that folder to make sure that all peers (in HA mode) will update folder title and
// clean up the current state // clean up the current state
bus.AddEventListener(func(ctx context.Context, evt *events.FolderTitleUpdated) error { bus.AddEventListener(func(ctx context.Context, evt *events.FolderFullPathUpdated) error {
logger.Info("Got folder title updated event. updating rules in the folder", "folderUID", evt.UID) logger.Info("Got folder full path updated event. updating rules in the folders", "folderUIDs", evt.UIDs)
_, err := dbStore.IncreaseVersionForAllRulesInNamespace(ctx, evt.OrgID, evt.UID) updatedKeys, err := dbStore.IncreaseVersionForAllRulesInNamespaces(ctx, evt.OrgID, evt.UIDs)
if err != nil { if err != nil {
logger.Error("Failed to update alert rules in the folder after its title was changed", "error", err, "folderUID", evt.UID, "folder", evt.Title) logger.Error("Failed to update alert rules in the folders after their full paths were changed", "error", err, "folderUIDs", evt.UIDs, "orgID", evt.OrgID)
return err return err
} }
logger.Info("Updated version for alert rules", "keys", updatedKeys)
return nil return nil
}) })
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil" "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
@@ -25,37 +26,52 @@ import (
) )
func Test_subscribeToFolderChanges(t *testing.T) { func Test_subscribeToFolderChanges(t *testing.T) {
getRecordedCommand := func(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
results := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
c, ok := cmd.(fakes.GenericRecordedQuery)
if !ok || c.Name != "IncreaseVersionForAllRulesInNamespaces" {
return nil, false
}
return c, ok
})
var result []fakes.GenericRecordedQuery
for _, cmd := range results {
result = append(result, cmd.(fakes.GenericRecordedQuery))
}
return result
}
orgID := rand.Int63() orgID := rand.Int63()
folder := &folder.Folder{ folder1 := &folder.Folder{
UID: util.GenerateShortUID(),
Title: "Folder" + util.GenerateShortUID(),
}
folder2 := &folder.Folder{
UID: util.GenerateShortUID(), UID: util.GenerateShortUID(),
Title: "Folder" + util.GenerateShortUID(), Title: "Folder" + util.GenerateShortUID(),
} }
gen := models.RuleGen gen := models.RuleGen
rules := gen.With(gen.WithOrgID(orgID), gen.WithNamespace(folder)).GenerateManyRef(5) rules := gen.With(gen.WithOrgID(orgID), gen.WithNamespace(folder1)).GenerateManyRef(5)
bus := bus.ProvideBus(tracing.InitializeTracerForTest()) bus := bus.ProvideBus(tracing.InitializeTracerForTest())
db := fakes.NewRuleStore(t) db := fakes.NewRuleStore(t)
db.Folders[orgID] = append(db.Folders[orgID], folder) db.Folders[orgID] = append(db.Folders[orgID], folder1)
db.PutRule(context.Background(), rules...) db.PutRule(context.Background(), rules...)
subscribeToFolderChanges(log.New("test"), bus, db) subscribeToFolderChanges(log.New("test"), bus, db)
err := bus.Publish(context.Background(), &events.FolderTitleUpdated{ err := bus.Publish(context.Background(), &events.FolderFullPathUpdated{
Timestamp: time.Now(), Timestamp: time.Now(),
Title: "Folder" + util.GenerateShortUID(), UIDs: []string{folder1.UID, folder2.UID},
UID: folder.UID,
OrgID: orgID, OrgID: orgID,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Eventuallyf(t, func() bool { require.EventuallyWithT(t, func(c *assert.CollectT) {
return len(db.GetRecordedCommands(func(cmd any) (any, bool) { recordedCommands := getRecordedCommand(db)
c, ok := cmd.(fakes.GenericRecordedQuery) require.Len(c, recordedCommands, 1)
if !ok || c.Name != "IncreaseVersionForAllRulesInNamespace" { require.Equal(c, recordedCommands[0].Params[0].(int64), orgID)
return nil, false require.ElementsMatch(c, recordedCommands[0].Params[1].([]string), []string{folder1.UID, folder2.UID})
}
return c, true
})) > 0
}, time.Second, 10*time.Millisecond, "expected to call db store method but nothing was called") }, time.Second, 10*time.Millisecond, "expected to call db store method but nothing was called")
} }

View File

@@ -1479,7 +1479,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
_, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg) _, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg)
ac := acmock.New() ac := acmock.New()
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
folderService := folderimpl.ProvideService(ac, inProcBus, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) folderService := folderimpl.ProvideService(ac, inProcBus, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
ruleService := createAlertRuleService(t, folderService) ruleService := createAlertRuleService(t, folderService)
var orgID int64 = 1 var orgID int64 = 1

View File

@@ -75,16 +75,26 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI
}) })
} }
// IncreaseVersionForAllRulesInNamespace Increases version for all rules that have specified namespace. Returns all rules that belong to the namespace // IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace. Returns all rules that belong to the namespaces
func (st DBstore) IncreaseVersionForAllRulesInNamespace(ctx context.Context, orgID int64, namespaceUID string) ([]ngmodels.AlertRuleKeyWithVersion, error) { func (st DBstore) IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error) {
var keys []ngmodels.AlertRuleKeyWithVersion var keys []ngmodels.AlertRuleKeyWithVersion
err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
now := TimeNow() now := TimeNow()
_, err := sess.Exec("UPDATE alert_rule SET version = version + 1, updated = ? WHERE namespace_uid = ? AND org_id = ?", now, namespaceUID, orgID) namespaceUIDsArgs, in := getINSubQueryArgs(namespaceUIDs)
sql := fmt.Sprintf(
"UPDATE alert_rule SET version = version + 1, updated = ? WHERE org_id = ? AND namespace_uid IN (%s)",
strings.Join(in, ","),
)
args := make([]interface{}, 0, 3+len(namespaceUIDsArgs))
args = append(args, sql, now, orgID)
args = append(args, namespaceUIDsArgs...)
_, err := sess.Exec(args...)
if err != nil { if err != nil {
return err return err
} }
return sess.Table(ngmodels.AlertRule{}).Where("namespace_uid = ? AND org_id = ?", namespaceUID, orgID).Find(&keys)
return sess.Table(ngmodels.AlertRule{}).Where("org_id = ?", orgID).In("namespace_uid", namespaceUIDs).Find(&keys)
}) })
return keys, err return keys, err
} }

View File

@@ -1184,6 +1184,58 @@ func TestIntegrationRuleGroupsCaseSensitive(t *testing.T) {
}) })
} }
func TestIncreaseVersionForAllRulesInNamespaces(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
cfg := setting.NewCfg()
cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{BaseInterval: time.Duration(rand.Int63n(100)+1) * time.Second}
sqlStore := db.InitTestReplDB(t)
store := &DBstore{
SQLStore: sqlStore,
Cfg: cfg.UnifiedAlerting,
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
Logger: &logtest.Fake{},
}
orgID := int64(1)
gen := models.RuleGen
gen = gen.With(gen.WithIntervalMatching(store.Cfg.BaseInterval)).With(gen.WithOrgID(orgID))
alertRules := []*models.AlertRule{}
for i := 0; i < 5; i++ {
alertRules = append(alertRules, createRule(t, store, gen))
}
alertRuleNamespaceUIDs := make([]string, 0, len(alertRules))
for _, rule := range alertRules {
alertRuleNamespaceUIDs = append(alertRuleNamespaceUIDs, rule.NamespaceUID)
}
alertRuleInAnotherNamespace := createRule(t, store, gen)
requireAlertRuleVersion := func(t *testing.T, ruleID int64, orgID int64, expectedVersion int64) {
t.Helper()
dbrule := &models.AlertRule{}
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
exist, err := sess.Table(models.AlertRule{}).ID(ruleID).Get(dbrule)
require.Truef(t, exist, fmt.Sprintf("rule with ID %d does not exist", ruleID))
return err
})
require.NoError(t, err)
require.Equal(t, expectedVersion, dbrule.Version)
}
t.Run("should increase version for all rules", func(t *testing.T) {
_, err := store.IncreaseVersionForAllRulesInNamespaces(context.Background(), orgID, alertRuleNamespaceUIDs)
require.NoError(t, err)
for _, rule := range alertRules {
requireAlertRuleVersion(t, rule.ID, orgID, rule.Version+1)
}
// this rule's version should not be changed
requireAlertRuleVersion(t, alertRuleInAnotherNamespace.ID, orgID, alertRuleInAnotherNamespace.Version)
})
}
// createAlertRule creates an alert rule in the database and returns it. // createAlertRule creates an alert rule in the database and returns it.
// If a generator is not specified, uniqueness of primary key is not guaranteed. // If a generator is not specified, uniqueness of primary key is not guaranteed.
func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule { func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule {

View File

@@ -315,19 +315,24 @@ func (f *RuleStore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceU
return nil return nil
} }
func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, orgID int64, namespaceUID string) ([]models.AlertRuleKeyWithVersion, error) { func (f *RuleStore) IncreaseVersionForAllRulesInNamespaces(_ context.Context, orgID int64, namespaceUIDs []string) ([]models.AlertRuleKeyWithVersion, error) {
f.mtx.Lock() f.mtx.Lock()
defer f.mtx.Unlock() defer f.mtx.Unlock()
f.RecordedOps = append(f.RecordedOps, GenericRecordedQuery{ f.RecordedOps = append(f.RecordedOps, GenericRecordedQuery{
Name: "IncreaseVersionForAllRulesInNamespace", Name: "IncreaseVersionForAllRulesInNamespaces",
Params: []any{orgID, namespaceUID}, Params: []any{orgID, namespaceUIDs},
}) })
var result []models.AlertRuleKeyWithVersion var result []models.AlertRuleKeyWithVersion
namespaceUIDsMap := make(map[string]struct{}, len(namespaceUIDs))
for _, namespaceUID := range namespaceUIDs {
namespaceUIDsMap[namespaceUID] = struct{}{}
}
for _, rule := range f.Rules[orgID] { for _, rule := range f.Rules[orgID] {
if rule.NamespaceUID == namespaceUID && rule.OrgID == orgID { if _, ok := namespaceUIDsMap[rule.NamespaceUID]; ok && rule.OrgID == orgID {
rule.Version++ rule.Version++
rule.Updated = time.Now() rule.Updated = time.Now()
result = append(result, models.AlertRuleKeyWithVersion{ result = append(result, models.AlertRuleKeyWithVersion{

View File

@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
@@ -26,7 +27,7 @@ import (
func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) folder.Service { func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) folder.Service {
tb.Helper() tb.Helper()
return folderimpl.ProvideService(ac, bus, dashboardStore, folderStore, db, features, supportbundlestest.NewFakeBundleService(), nil) return folderimpl.ProvideService(ac, bus, dashboardStore, folderStore, db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
} }
func SetupDashboardService(tb testing.TB, sqlStore db.ReplDB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { func SetupDashboardService(tb testing.TB, sqlStore db.ReplDB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) {

View File

@@ -822,7 +822,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil)) dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil))
require.NoError(t, err) require.NoError(t, err)
folderSvc := folderimpl.ProvideService(actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil) folderSvc := folderimpl.ProvideService(actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
// create parent folder // create parent folder
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{

View File

@@ -81,7 +81,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe
dashboardWriteStore, err := database.ProvideDashboardStore(store, cfg, features, tagimpl.ProvideService(store), quotaService) dashboardWriteStore, err := database.ProvideDashboardStore(store, cfg, features, tagimpl.ProvideService(store), quotaService)
require.NoError(b, err) require.NoError(b, err)
folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, supportbundlestest.NewFakeBundleService(), nil) folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
origNewGuardian := guardian.New origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})