Alerting: Update API to use folders' full paths (#81214)

* update GetUserVisibleNamespaces to use FolderSeriver
* update GetNamespaceByUID to use FolderService.GetFolders
* update GetAlertRulesForScheduling to use FolderService.GetFolders 

* Update API and GetAlertRulesForScheduling to use the folder's full path
* get full path of folder in RouteTestGrafanaRuleConfig

* fix escaping of titles for MySQL
This commit is contained in:
Yuri Tseretyan
2024-02-06 17:12:13 -05:00
committed by GitHub
parent 6f8852095e
commit 47546a4c72
18 changed files with 217 additions and 222 deletions

View File

@@ -7,18 +7,17 @@ import (
"strings"
"github.com/google/uuid"
"golang.org/x/exp/maps"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/org"
"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/util"
)
@@ -434,58 +433,34 @@ func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespa
})
}
// GetUserVisibleNamespaces returns the folders that are visible to the user and have at least one alert in it
// GetUserVisibleNamespaces returns the folders that are visible to the user
func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user identity.Requester) (map[string]*folder.Folder, error) {
namespaceMap := make(map[string]*folder.Folder)
searchQuery := dashboards.FindPersistedDashboardsQuery{
OrgId: orgID,
folders, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{
OrgID: orgID,
WithFullpath: true,
SignedInUser: user,
Type: searchstore.TypeAlertFolder,
Limit: -1,
Permission: dashboardaccess.PERMISSION_VIEW,
Sort: model.SortOption{},
Filters: []any{
searchstore.FolderWithAlertsFilter{},
},
})
if err != nil {
return nil, err
}
var page int64 = 1
for {
query := searchQuery
query.Page = page
proj, err := st.DashboardService.FindDashboards(ctx, &query)
if err != nil {
return nil, err
}
if len(proj) == 0 {
break
}
for _, hit := range proj {
if !hit.IsFolder {
continue
}
namespaceMap[hit.UID] = &folder.Folder{
UID: hit.UID,
Title: hit.Title,
ParentUID: hit.FolderUID,
}
}
page += 1
namespaceMap := make(map[string]*folder.Folder)
for _, f := range folders {
namespaceMap[f.UID] = f
}
return namespaceMap, nil
}
// GetNamespaceByUID is a handler for retrieving a namespace by its UID. Alerting rules follow a Grafana folder-like structure which we call namespaces.
func (st DBstore) GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error) {
folder, err := st.FolderService.Get(ctx, &folder.GetFolderQuery{OrgID: orgID, UID: &uid, SignedInUser: user})
f, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{OrgID: orgID, UIDs: []string{uid}, WithFullpath: true, SignedInUser: user})
if err != nil {
return nil, err
}
return folder, nil
if len(f) == 0 {
return nil, dashboards.ErrFolderAccessDenied
}
return f[0], nil
}
func (st DBstore) GetAlertRulesKeysForScheduling(ctx context.Context) ([]ngmodels.AlertRuleKeyWithVersion, error) {
@@ -513,12 +488,6 @@ func (st DBstore) GetAlertRulesKeysForScheduling(ctx context.Context) ([]ngmodel
// GetAlertRulesForScheduling returns a short version of all alert rules except those that belong to an excluded list of organizations
func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.GetAlertRulesForSchedulingQuery) error {
var folders []struct {
OrgId int64
Uid string
Title string
ParentUid string
}
var rules []*ngmodels.AlertRule
return st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
var disabledOrgs []int64
@@ -566,22 +535,36 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel
query.ResultRules = rules
if query.PopulateFolders {
foldersSql := sess.Table("folder").Alias("d").Select("d.org_id, d.uid, d.title, d.parent_uid").
Where(`EXISTS (SELECT 1 FROM alert_rule a WHERE d.uid = a.namespace_uid AND d.org_id = a.org_id)`)
if len(disabledOrgs) > 0 {
foldersSql.NotIn("org_id", disabledOrgs)
}
if err := foldersSql.Find(&folders); err != nil {
return fmt.Errorf("failed to fetch a list of folders that contain alert rules: %w", err)
}
query.ResultFoldersTitles = make(map[ngmodels.FolderKey]string, len(folders))
for _, f := range folders {
key := ngmodels.FolderKey{
OrgID: f.OrgId,
UID: f.Uid,
query.ResultFoldersTitles = map[ngmodels.FolderKey]string{}
uids := map[int64]map[string]struct{}{}
for _, r := range rules {
om, ok := uids[r.OrgID]
if !ok {
om = make(map[string]struct{})
uids[r.OrgID] = om
}
om[r.NamespaceUID] = struct{}{}
}
for orgID, uids := range uids {
schedulerUser := accesscontrol.BackgroundUser("grafana_scheduler", orgID, org.RoleAdmin,
[]accesscontrol.Permission{
{
Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll,
},
})
folders, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{
OrgID: orgID,
UIDs: maps.Keys(uids),
WithFullpath: true,
SignedInUser: schedulerUser,
})
if err != nil {
return fmt.Errorf("failed to fetch a list of folders that contain alert rules: %w", err)
}
for _, f := range folders {
query.ResultFoldersTitles[ngmodels.FolderKey{OrgID: f.OrgID, UID: f.UID}] = f.Fullpath
}
query.ResultFoldersTitles[key] = ngmodels.GetNamespaceKey(f.ParentUid, f.Title)
}
}
return nil

View File

@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
@@ -44,7 +45,7 @@ func TestIntegrationUpdateAlertRules(t *testing.T) {
store := &DBstore{
SQLStore: sqlStore,
Cfg: cfg.UnifiedAlerting,
FolderService: setupFolderService(t, sqlStore, cfg),
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
Logger: &logtest.Fake{},
}
generator := models.AlertRuleGen(withIntervalMatching(store.Cfg.BaseInterval), models.WithUniqueID())
@@ -98,7 +99,7 @@ func TestIntegrationUpdateAlertRulesWithUniqueConstraintViolation(t *testing.T)
store := &DBstore{
SQLStore: sqlStore,
Cfg: cfg.UnifiedAlerting,
FolderService: setupFolderService(t, sqlStore, cfg),
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
Logger: &logtest.Fake{},
}
@@ -332,7 +333,7 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
store := &DBstore{
SQLStore: sqlStore,
Cfg: cfg.UnifiedAlerting,
FolderService: setupFolderService(t, sqlStore, cfg),
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
FeatureToggles: featuremgmt.WithFeatures(),
}
@@ -341,9 +342,12 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
rule2 := createRule(t, store, generator)
parentFolderUid := uuid.NewString()
createFolder(t, store, parentFolderUid, "Very Parent Folder", rule1.OrgID, "")
createFolder(t, store, rule1.NamespaceUID, rule1.Title, rule1.OrgID, parentFolderUid)
createFolder(t, store, rule2.NamespaceUID, rule2.Title, rule2.OrgID, "")
parentFolderTitle := "Very Parent Folder"
createFolder(t, store, parentFolderUid, parentFolderTitle, rule1.OrgID, "")
rule1FolderTitle := "folder-" + rule1.Title
rule2FolderTitle := "folder-" + rule2.Title
createFolder(t, store, rule1.NamespaceUID, rule1FolderTitle, rule1.OrgID, parentFolderUid)
createFolder(t, store, rule2.NamespaceUID, rule2FolderTitle, rule2.OrgID, "")
createFolder(t, store, rule2.NamespaceUID, "same UID folder", generator().OrgID, "") // create a folder with the same UID but in the different org
@@ -353,6 +357,7 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
ruleGroups []string
disabledOrgs []int64
folders map[models.FolderKey]string
flags []string
}{
{
name: "without a rule group filter, it returns all created rules",
@@ -371,13 +376,13 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
{
name: "with populate folders enabled, it returns them",
rules: []string{rule1.Title, rule2.Title},
folders: map[models.FolderKey]string{rule1.GetFolderKey(): models.GetNamespaceKey(parentFolderUid, rule1.Title), rule2.GetFolderKey(): rule2.Title},
folders: map[models.FolderKey]string{rule1.GetFolderKey(): rule1FolderTitle, rule2.GetFolderKey(): rule2FolderTitle},
},
{
name: "with populate folders enabled and a filter on orgs, it only returns selected information",
rules: []string{rule1.Title},
disabledOrgs: []int64{rule2.OrgID},
folders: map[models.FolderKey]string{rule1.GetFolderKey(): models.GetNamespaceKey(parentFolderUid, rule1.Title)},
folders: map[models.FolderKey]string{rule1.GetFolderKey(): rule1FolderTitle},
},
}
@@ -414,6 +419,20 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
}
})
}
t.Run("when nested folders are enabled folders should contain full path", func(t *testing.T) {
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
query := &models.GetAlertRulesForSchedulingQuery{
PopulateFolders: true,
}
require.NoError(t, store.GetAlertRulesForScheduling(context.Background(), query))
expected := map[models.FolderKey]string{
rule1.GetFolderKey(): parentFolderTitle + "/" + rule1FolderTitle,
rule2.GetFolderKey(): rule2FolderTitle,
}
require.Equal(t, expected, query.ResultFoldersTitles)
})
}
func withIntervalMatching(baseInterval time.Duration) func(*models.AlertRule) {
@@ -430,7 +449,7 @@ func TestIntegration_CountAlertRules(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
store := &DBstore{SQLStore: sqlStore, FolderService: setupFolderService(t, sqlStore, cfg)}
store := &DBstore{SQLStore: sqlStore, FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())}
rule := createRule(t, store, nil)
tests := map[string]struct {
@@ -479,7 +498,7 @@ func TestIntegration_DeleteInFolder(t *testing.T) {
cfg := setting.NewCfg()
store := &DBstore{
SQLStore: sqlStore,
FolderService: setupFolderService(t, sqlStore, cfg),
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
Logger: log.New("test-dbstore"),
}
rule := createRule(t, store, nil)
@@ -512,7 +531,7 @@ func TestIntegration_GetNamespaceByUID(t *testing.T) {
cfg := setting.NewCfg()
store := &DBstore{
SQLStore: sqlStore,
FolderService: setupFolderService(t, sqlStore, cfg),
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
Logger: log.New("test-dbstore"),
}
@@ -524,13 +543,36 @@ func TestIntegration_GetNamespaceByUID(t *testing.T) {
}
uid := uuid.NewString()
title := "folder-title"
createFolder(t, store, uid, title, 1, "")
parentUid := uuid.NewString()
title := "folder/title"
parentTitle := "parent-title"
createFolder(t, store, parentUid, parentTitle, 1, "")
createFolder(t, store, uid, title, 1, parentUid)
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
require.NoError(t, err)
require.Equal(t, title, actual.Title)
require.Equal(t, uid, actual.UID)
require.Equal(t, title, actual.Fullpath)
t.Run("error when user does not have permissions", func(t *testing.T) {
someUser := &user.SignedInUser{
UserID: 2,
OrgID: 1,
OrgRole: org.RoleViewer,
}
_, err = store.GetNamespaceByUID(context.Background(), uid, 1, someUser)
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
})
t.Run("when nested folders are enabled full path should be populated with correct value", func(t *testing.T) {
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
require.NoError(t, err)
require.Equal(t, title, actual.Title)
require.Equal(t, uid, actual.UID)
require.Equal(t, "parent-title/folder\\/title", actual.Fullpath)
})
}
func TestIntegrationInsertAlertRules(t *testing.T) {
@@ -543,7 +585,7 @@ func TestIntegrationInsertAlertRules(t *testing.T) {
cfg.UnifiedAlerting.BaseInterval = 1 * time.Second
store := &DBstore{
SQLStore: sqlStore,
FolderService: setupFolderService(t, sqlStore, cfg),
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
Logger: log.New("test-dbstore"),
Cfg: cfg.UnifiedAlerting,
}
@@ -630,11 +672,11 @@ func createFolder(t *testing.T, store *DBstore, uid, title string, orgID int64,
require.NoError(t, err)
}
func setupFolderService(t *testing.T, sqlStore *sqlstore.SQLStore, cfg *setting.Cfg) folder.Service {
func setupFolderService(t *testing.T, sqlStore *sqlstore.SQLStore, cfg *setting.Cfg, features featuremgmt.FeatureToggles) folder.Service {
tracer := tracing.InitializeTracerForTest()
inProcBus := bus.ProvideBus(tracer)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
_, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg)
return testutil.SetupFolderService(t, cfg, sqlStore, dashboardStore, folderStore, inProcBus)
return testutil.SetupFolderService(t, cfg, sqlStore, dashboardStore, folderStore, inProcBus, features, &actest.FakeAccessControl{})
}