Alerting: Export and provisioning rules into subfolders (#77450)

* Folders: Optionally include fullpath in service responses
* Alerting: Export folder fullpath instead of title
* Escape separator in folder title
* Add support for provisiong alret rules into subfolders
* Use FolderService for creating folders during provisioning
* Export WithFullpath() folder service function

---------

Co-authored-by: Tania B <yalyna.ts@gmail.com>
Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
Sofia Papagiannaki 2024-05-31 11:09:20 +03:00 committed by GitHub
parent e1aedb65b3
commit 17ca61d7f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 435 additions and 164 deletions

View File

@ -333,7 +333,6 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *folder.CreateFolderCommand) (*folder.Folder, error) { func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *folder.CreateFolderCommand) (*folder.Folder, error) {
dto.SignedInUser = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions) dto.SignedInUser = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions)
f, err := dr.folderService.Create(ctx, dto) f, err := dr.folderService.Create(ctx, dto)
if err != nil { if err != nil {
dr.log.Error("failed to create folder for provisioned dashboards", "folder", dto.Title, "org", dto.OrgID, "err", err) dr.log.Error("failed to create folder for provisioned dashboards", "folder", dto.Title, "org", dto.OrgID, "err", err)

View File

@ -98,6 +98,8 @@ func (d *DashboardFolderStoreImpl) GetFolderByUID(ctx context.Context, orgID int
return dashboards.FromDashboard(&dashboard), nil return dashboards.FromDashboard(&dashboard), nil
} }
// GetFolders returns all folders for the given orgID and UIDs.
// If no UIDs are provided then all folders for the org are returned.
func (d *DashboardFolderStoreImpl) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) { func (d *DashboardFolderStoreImpl) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) {
m := make(map[string]*folder.Folder, len(uids)) m := make(map[string]*folder.Folder, len(uids))
if len(uids) == 0 { if len(uids) == 0 {

View File

@ -34,6 +34,8 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
const FULLPATH_SEPARATOR = "/"
type Service struct { type Service struct {
store store store store
db db.DB db db.DB
@ -1109,6 +1111,36 @@ func (s *Service) buildSaveDashboardCommand(ctx context.Context, dto *dashboards
return cmd, nil return cmd, nil
} }
// SplitFullpath splits a string into an array of strings using the FULLPATH_SEPARATOR as the delimiter.
// It handles escape characters by appending the separator and the new string if the current string ends with an escape character.
// The resulting array does not contain empty strings.
func SplitFullpath(s string) []string {
splitStrings := strings.Split(s, FULLPATH_SEPARATOR)
result := make([]string, 0)
current := ""
for _, str := range splitStrings {
if strings.HasSuffix(current, "\\") {
// If the current string ends with an escape character, append the separator and the new string
current = current[:len(current)-1] + FULLPATH_SEPARATOR + str
} else {
// If the current string does not end with an escape character, append the current string to the result and start a new current string
if current != "" {
result = append(result, current)
}
current = str
}
}
// Append the last string to the result
if current != "" {
result = append(result, current)
}
return result
}
// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard // getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard
// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck() // It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck()
func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user identity.Requester) (guardian.DashboardGuardian, error) { func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user identity.Requester) (guardian.DashboardGuardian, error) {

View File

@ -2216,3 +2216,54 @@ func createRule(t *testing.T, store *ngstore.DBstore, folderUID, title string) *
return &rule return &rule
} }
func TestSplitFullpath(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{
name: "empty string",
input: "",
expected: []string{},
},
{
name: "root folder",
input: "/",
expected: []string{},
},
{
name: "single folder",
input: "folder",
expected: []string{"folder"},
},
{
name: "single folder with leading slash",
input: "/folder",
expected: []string{"folder"},
},
{
name: "nested folder",
input: "folder/subfolder/subsubfolder",
expected: []string{"folder", "subfolder", "subsubfolder"},
},
{
name: "escaped slashes",
input: "folder\\/with\\/slashes",
expected: []string{"folder/with/slashes"},
},
{
name: "nested folder with escaped slashes",
input: "folder\\/with\\/slashes/subfolder\\/with\\/slashes",
expected: []string{"folder/with/slashes", "subfolder/with/slashes"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := SplitFullpath(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/ngalert/api/hcl" "github.com/grafana/grafana/pkg/services/ngalert/api/hcl"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -28,6 +29,7 @@ type ProvisioningSrv struct {
templates TemplateService templates TemplateService
muteTimings MuteTimingService muteTimings MuteTimingService
alertRules AlertRuleService alertRules AlertRuleService
folderSvc folder.Service
} }
type ContactPointService interface { type ContactPointService interface {
@ -66,9 +68,9 @@ type AlertRuleService interface {
GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error) GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error)
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error
DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error
GetAlertRuleWithFolderTitle(ctx context.Context, user identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderTitle, error) GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error)
GetAlertRuleGroupWithFolderTitle(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderTitle, error) GetAlertRuleGroupWithFolderFullpath(ctx context.Context, u identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderFullpath, error)
GetAlertGroupsWithFolderTitle(ctx context.Context, user identity.Requester, folderUIDs []string) ([]alerting_models.AlertRuleGroupWithFolderTitle, error) GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, folderUIDs []string) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error)
} }
func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response { func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response {
@ -422,15 +424,15 @@ func (srv *ProvisioningSrv) RouteGetAlertRulesExport(c *contextmodel.ReqContext)
return srv.RouteGetAlertRuleGroupExport(c, folderUIDs[0], group) return srv.RouteGetAlertRuleGroupExport(c, folderUIDs[0], group)
} }
groupsWithTitle, err := srv.alertRules.GetAlertGroupsWithFolderTitle(c.Req.Context(), c.SignedInUser, folderUIDs) groupsWithFullpath, err := srv.alertRules.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, folderUIDs)
if err != nil { if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rules", err) return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rules", err)
} }
if len(groupsWithTitle) == 0 { if len(groupsWithFullpath) == 0 {
return response.Empty(http.StatusNotFound) return response.Empty(http.StatusNotFound)
} }
e, err := AlertingFileExportFromAlertRuleGroupWithFolderTitle(groupsWithTitle) e, err := AlertingFileExportFromAlertRuleGroupWithFolderFullpath(groupsWithFullpath)
if err != nil { if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "failed to create alerting file export", err) return response.ErrOrFallback(http.StatusInternalServerError, "failed to create alerting file export", err)
} }
@ -440,12 +442,12 @@ func (srv *ProvisioningSrv) RouteGetAlertRulesExport(c *contextmodel.ReqContext)
// RouteGetAlertRuleGroupExport retrieves the given alert rule group in a format compatible with file provisioning. // RouteGetAlertRuleGroupExport retrieves the given alert rule group in a format compatible with file provisioning.
func (srv *ProvisioningSrv) RouteGetAlertRuleGroupExport(c *contextmodel.ReqContext, folder string, group string) response.Response { func (srv *ProvisioningSrv) RouteGetAlertRuleGroupExport(c *contextmodel.ReqContext, folder string, group string) response.Response {
g, err := srv.alertRules.GetAlertRuleGroupWithFolderTitle(c.Req.Context(), c.SignedInUser, folder, group) g, err := srv.alertRules.GetAlertRuleGroupWithFolderFullpath(c.Req.Context(), c.SignedInUser, folder, group)
if err != nil { if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rule group", err) return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rule group", err)
} }
e, err := AlertingFileExportFromAlertRuleGroupWithFolderTitle([]alerting_models.AlertRuleGroupWithFolderTitle{g}) e, err := AlertingFileExportFromAlertRuleGroupWithFolderFullpath([]alerting_models.AlertRuleGroupWithFolderFullpath{g})
if err != nil { if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "failed to create alerting file export", err) return response.ErrOrFallback(http.StatusInternalServerError, "failed to create alerting file export", err)
} }
@ -455,7 +457,7 @@ func (srv *ProvisioningSrv) RouteGetAlertRuleGroupExport(c *contextmodel.ReqCont
// RouteGetAlertRuleExport retrieves the given alert rule in a format compatible with file provisioning. // RouteGetAlertRuleExport retrieves the given alert rule in a format compatible with file provisioning.
func (srv *ProvisioningSrv) RouteGetAlertRuleExport(c *contextmodel.ReqContext, UID string) response.Response { func (srv *ProvisioningSrv) RouteGetAlertRuleExport(c *contextmodel.ReqContext, UID string) response.Response {
rule, err := srv.alertRules.GetAlertRuleWithFolderTitle(c.Req.Context(), c.SignedInUser, UID) rule, err := srv.alertRules.GetAlertRuleWithFolderFullpath(c.Req.Context(), c.SignedInUser, UID)
if err != nil { if err != nil {
if errors.Is(err, alerting_models.ErrAlertRuleNotFound) { if errors.Is(err, alerting_models.ErrAlertRuleNotFound) {
return ErrResp(http.StatusNotFound, err, "") return ErrResp(http.StatusNotFound, err, "")
@ -463,8 +465,8 @@ func (srv *ProvisioningSrv) RouteGetAlertRuleExport(c *contextmodel.ReqContext,
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rules", err) return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rules", err)
} }
e, err := AlertingFileExportFromAlertRuleGroupWithFolderTitle([]alerting_models.AlertRuleGroupWithFolderTitle{ e, err := AlertingFileExportFromAlertRuleGroupWithFolderFullpath([]alerting_models.AlertRuleGroupWithFolderFullpath{
alerting_models.NewAlertRuleGroupWithFolderTitleFromRulesGroup(rule.AlertRule.GetGroupKey(), alerting_models.RulesGroup{&rule.AlertRule}, rule.FolderTitle), alerting_models.NewAlertRuleGroupWithFolderFullpathFromRulesGroup(rule.AlertRule.GetGroupKey(), alerting_models.RulesGroup{&rule.AlertRule}, rule.FolderFullpath),
}) })
if err != nil { if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")

View File

@ -19,23 +19,33 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/log/logtest" "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"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder" "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/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes" secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/tests/testsuite"
@ -317,6 +327,14 @@ func TestProvisioningApi(t *testing.T) {
rc.OrgID = 3 rc.OrgID = 3
rule := createTestAlertRule("rule", 1) rule := createTestAlertRule("rule", 1)
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: "folder-uid",
Title: "Folder Title",
OrgID: rc.OrgID,
SignedInUser: &user.SignedInUser{OrgID: rc.OrgID},
})
require.NoError(t, err)
response := sut.RoutePostAlertRule(&rc, rule) response := sut.RoutePostAlertRule(&rc, rule)
require.Equal(t, 201, response.Status()) require.Equal(t, 201, response.Status())
@ -330,7 +348,17 @@ func TestProvisioningApi(t *testing.T) {
uid := util.GenerateShortUID() uid := util.GenerateShortUID()
rule := createTestAlertRule("rule", 1) rule := createTestAlertRule("rule", 1)
rule.UID = uid rule.UID = uid
insertRuleInOrg(t, sut, rule, 3)
orgID := int64(3)
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: "folder-uid",
Title: "Folder Title",
OrgID: orgID,
SignedInUser: &user.SignedInUser{OrgID: orgID},
})
require.NoError(t, err)
insertRuleInOrg(t, sut, rule, orgID)
rc := createTestRequestCtx() rc := createTestRequestCtx()
rc.Req.Header = map[string][]string{"X-Disable-Provenance": {"hello"}} rc.Req.Header = map[string][]string{"X-Disable-Provenance": {"hello"}}
rc.OrgID = 3 rc.OrgID = 3
@ -1614,6 +1642,7 @@ type testEnvironment struct {
quotas provisioning.QuotaChecker quotas provisioning.QuotaChecker
prov provisioning.ProvisioningStore prov provisioning.ProvisioningStore
ac *recordingAccessControlFake ac *recordingAccessControlFake
user *user.SignedInUser
rulesAuthz *fakes.FakeRuleService rulesAuthz *fakes.FakeRuleService
} }
@ -1639,20 +1668,8 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
GetsConfig(models.AlertConfiguration{ GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: string(raw), AlertmanagerConfiguration: string(raw),
}) })
sqlStore := db.InitTestDB(t) sqlStore, cfg := db.InitTestDBWithCfg(t)
// init folder service with default folder
folderService := foldertest.NewFakeService()
folderService.ExpectedFolder = &folder.Folder{}
store := store.DBstore{
Logger: log,
SQLStore: sqlStore,
Cfg: setting.UnifiedAlertingSettings{
BaseInterval: time.Second * 10,
},
FolderService: folderService,
}
quotas := &provisioning.MockQuotaChecker{} quotas := &provisioning.MockQuotaChecker{}
quotas.EXPECT().LimitOK() quotas.EXPECT().LimitOK()
xact := &provisioning.NopTransactionManager{} xact := &provisioning.NopTransactionManager{}
@ -1675,6 +1692,49 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
}}, nil).Maybe() }}, nil).Maybe()
ac := &recordingAccessControlFake{} ac := &recordingAccessControlFake{}
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil))
require.NoError(t, err)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
folderService := folderimpl.ProvideService(actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil)
store := store.DBstore{
Logger: log,
SQLStore: sqlStore,
Cfg: setting.UnifiedAlertingSettings{
BaseInterval: time.Second * 10,
},
FolderService: folderService,
}
user := &user.SignedInUser{
OrgID: 1,
/*
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionFoldersCreate: {}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}},
},
*/
}
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
t.Cleanup(func() {
guardian.New = origNewGuardian
})
parent, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: 1,
UID: "folder-uid",
Title: "Folder Title",
SignedInUser: user,
})
require.NoError(t, err)
_, err = folderService.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: 1,
UID: "folder-uid2",
Title: "Folder Title2",
ParentUID: parent.UID,
SignedInUser: user,
})
require.NoError(t, err)
ruleAuthz := &fakes.FakeRuleService{} ruleAuthz := &fakes.FakeRuleService{}
@ -1689,6 +1749,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
prov: prov, prov: prov,
quotas: quotas, quotas: quotas,
ac: ac, ac: ac,
user: user,
rulesAuthz: ruleAuthz, rulesAuthz: ruleAuthz,
} }
} }
@ -1710,7 +1771,8 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store), contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store),
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log), templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log), muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log),
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.folderService, env.dashboardService, env.quotas, env.xact, 60, 10, 100, env.log, &provisioning.NotificationSettingsValidatorProviderFake{}, env.rulesAuthz), alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.folderService, env.quotas, env.xact, 60, 10, 100, env.log, &provisioning.NotificationSettingsValidatorProviderFake{}, env.rulesAuthz),
folderSvc: env.folderService,
} }
} }
@ -1725,6 +1787,9 @@ func createTestRequestCtx() contextmodel.ReqContext {
}, },
SignedInUser: &user.SignedInUser{ SignedInUser: &user.SignedInUser{
OrgID: 1, OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}},
},
}, },
Logger: &logtest.Fake{}, Logger: &logtest.Fake{},
} }

View File

@ -35,9 +35,9 @@ func (srv RulerSrv) ExportFromPayload(c *contextmodel.ReqContext, ruleGroupConfi
rules = append(rules, optional.AlertRule) rules = append(rules, optional.AlertRule)
} }
groupsWithTitle := ngmodels.NewAlertRuleGroupWithFolderTitle(rules[0].GetGroupKey(), rules, namespace.Title) groupsWithFullpath := ngmodels.NewAlertRuleGroupWithFolderFullpath(rules[0].GetGroupKey(), rules, namespace.Fullpath)
e, err := AlertingFileExportFromAlertRuleGroupWithFolderTitle([]ngmodels.AlertRuleGroupWithFolderTitle{groupsWithTitle}) e, err := AlertingFileExportFromAlertRuleGroupWithFolderFullpath([]ngmodels.AlertRuleGroupWithFolderFullpath{groupsWithFullpath})
if err != nil { if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
} }
@ -54,16 +54,16 @@ func (srv RulerSrv) ExportRules(c *contextmodel.ReqContext) response.Response {
group := c.Query("group") group := c.Query("group")
uid := c.Query("ruleUid") uid := c.Query("ruleUid")
var groups []ngmodels.AlertRuleGroupWithFolderTitle var groups []ngmodels.AlertRuleGroupWithFolderFullpath
if uid != "" { if uid != "" {
if group != "" || len(folderUIDs) > 0 { if group != "" || len(folderUIDs) > 0 {
return ErrResp(http.StatusBadRequest, errors.New("group and folder should not be specified when a single rule is requested"), "") return ErrResp(http.StatusBadRequest, errors.New("group and folder should not be specified when a single rule is requested"), "")
} }
rulesGroup, err := srv.getRuleWithFolderTitleByRuleUid(c, uid) rulesGroup, err := srv.getRuleWithFolderFullpathByRuleUid(c, uid)
if err != nil { if err != nil {
return errorToResponse(err) return errorToResponse(err)
} }
groups = []ngmodels.AlertRuleGroupWithFolderTitle{rulesGroup} groups = []ngmodels.AlertRuleGroupWithFolderFullpath{rulesGroup}
} else if group != "" { } else if group != "" {
if len(folderUIDs) != 1 || folderUIDs[0] == "" { if len(folderUIDs) != 1 || folderUIDs[0] == "" {
return ErrResp(http.StatusBadRequest, return ErrResp(http.StatusBadRequest,
@ -71,7 +71,7 @@ func (srv RulerSrv) ExportRules(c *contextmodel.ReqContext) response.Response {
"", "",
) )
} }
rulesGroup, err := srv.getRuleGroupWithFolderTitle(c, ngmodels.AlertRuleGroupKey{ rulesGroup, err := srv.getRuleGroupWithFolderFullPath(c, ngmodels.AlertRuleGroupKey{
OrgID: c.SignedInUser.GetOrgID(), OrgID: c.SignedInUser.GetOrgID(),
NamespaceUID: folderUIDs[0], NamespaceUID: folderUIDs[0],
RuleGroup: group, RuleGroup: group,
@ -79,10 +79,10 @@ func (srv RulerSrv) ExportRules(c *contextmodel.ReqContext) response.Response {
if err != nil { if err != nil {
return errorToResponse(err) return errorToResponse(err)
} }
groups = []ngmodels.AlertRuleGroupWithFolderTitle{rulesGroup} groups = []ngmodels.AlertRuleGroupWithFolderFullpath{rulesGroup}
} else { } else {
var err error var err error
groups, err = srv.getRulesWithFolderTitleInFolders(c, folderUIDs) groups, err = srv.getRulesWithFolderFullPathInFolders(c, folderUIDs)
if err != nil { if err != nil {
return errorToResponse(err) return errorToResponse(err)
} }
@ -95,45 +95,45 @@ func (srv RulerSrv) ExportRules(c *contextmodel.ReqContext) response.Response {
// sort result so the response is always stable // sort result so the response is always stable
ngmodels.SortAlertRuleGroupWithFolderTitle(groups) ngmodels.SortAlertRuleGroupWithFolderTitle(groups)
e, err := AlertingFileExportFromAlertRuleGroupWithFolderTitle(groups) e, err := AlertingFileExportFromAlertRuleGroupWithFolderFullpath(groups)
if err != nil { if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
} }
return exportResponse(c, e) return exportResponse(c, e)
} }
// getRuleWithFolderTitleByRuleUid calls getAuthorizedRuleByUid and combines its result with folder (aka namespace) title. // getRuleWithFolderFullpathByRuleUid calls getAuthorizedRuleByUid and combines its result with folder (aka namespace) title.
func (srv RulerSrv) getRuleWithFolderTitleByRuleUid(c *contextmodel.ReqContext, ruleUID string) (ngmodels.AlertRuleGroupWithFolderTitle, error) { func (srv RulerSrv) getRuleWithFolderFullpathByRuleUid(c *contextmodel.ReqContext, ruleUID string) (ngmodels.AlertRuleGroupWithFolderFullpath, error) {
rule, err := srv.getAuthorizedRuleByUid(c.Req.Context(), c, ruleUID) rule, err := srv.getAuthorizedRuleByUid(c.Req.Context(), c, ruleUID)
if err != nil { if err != nil {
return ngmodels.AlertRuleGroupWithFolderTitle{}, err return ngmodels.AlertRuleGroupWithFolderFullpath{}, err
} }
namespace, err := srv.store.GetNamespaceByUID(c.Req.Context(), rule.NamespaceUID, c.SignedInUser.GetOrgID(), c.SignedInUser) namespace, err := srv.store.GetNamespaceByUID(c.Req.Context(), rule.NamespaceUID, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil { if err != nil {
return ngmodels.AlertRuleGroupWithFolderTitle{}, errors.Join(errFolderAccess, err) return ngmodels.AlertRuleGroupWithFolderFullpath{}, errors.Join(errFolderAccess, err)
} }
return ngmodels.NewAlertRuleGroupWithFolderTitle(rule.GetGroupKey(), []ngmodels.AlertRule{rule}, namespace.Title), nil return ngmodels.NewAlertRuleGroupWithFolderFullpath(rule.GetGroupKey(), []ngmodels.AlertRule{rule}, namespace.Fullpath), nil
} }
// getRuleGroupWithFolderTitle calls getAuthorizedRuleGroup and combines its result with folder (aka namespace) title. // getRuleGroupWithFolderFullPath calls getAuthorizedRuleGroup and combines its result with folder (aka namespace) title.
func (srv RulerSrv) getRuleGroupWithFolderTitle(c *contextmodel.ReqContext, ruleGroupKey ngmodels.AlertRuleGroupKey) (ngmodels.AlertRuleGroupWithFolderTitle, error) { func (srv RulerSrv) getRuleGroupWithFolderFullPath(c *contextmodel.ReqContext, ruleGroupKey ngmodels.AlertRuleGroupKey) (ngmodels.AlertRuleGroupWithFolderFullpath, error) {
namespace, err := srv.store.GetNamespaceByUID(c.Req.Context(), ruleGroupKey.NamespaceUID, c.SignedInUser.GetOrgID(), c.SignedInUser) namespace, err := srv.store.GetNamespaceByUID(c.Req.Context(), ruleGroupKey.NamespaceUID, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil { if err != nil {
return ngmodels.AlertRuleGroupWithFolderTitle{}, errors.Join(errFolderAccess, err) return ngmodels.AlertRuleGroupWithFolderFullpath{}, errors.Join(errFolderAccess, err)
} }
rules, err := srv.getAuthorizedRuleGroup(c.Req.Context(), c, ruleGroupKey) rules, err := srv.getAuthorizedRuleGroup(c.Req.Context(), c, ruleGroupKey)
if err != nil { if err != nil {
return ngmodels.AlertRuleGroupWithFolderTitle{}, err return ngmodels.AlertRuleGroupWithFolderFullpath{}, err
} }
if len(rules) == 0 { if len(rules) == 0 {
return ngmodels.AlertRuleGroupWithFolderTitle{}, ngmodels.ErrAlertRuleNotFound return ngmodels.AlertRuleGroupWithFolderFullpath{}, ngmodels.ErrAlertRuleNotFound
} }
return ngmodels.NewAlertRuleGroupWithFolderTitleFromRulesGroup(ruleGroupKey, rules, namespace.Title), nil return ngmodels.NewAlertRuleGroupWithFolderFullpathFromRulesGroup(ruleGroupKey, rules, namespace.Fullpath), nil
} }
// getRulesWithFolderTitleInFolders gets list of folders to which user has access, and then calls searchAuthorizedAlertRules. // getRulesWithFolderFullPathInFolders gets list of folders to which user has access, and then calls searchAuthorizedAlertRules.
// If argument folderUIDs is not empty it intersects it with the list of folders available for user and then retrieves rules that are in those folders. // If argument folderUIDs is not empty it intersects it with the list of folders available for user and then retrieves rules that are in those folders.
func (srv RulerSrv) getRulesWithFolderTitleInFolders(c *contextmodel.ReqContext, folderUIDs []string) ([]ngmodels.AlertRuleGroupWithFolderTitle, error) { func (srv RulerSrv) getRulesWithFolderFullPathInFolders(c *contextmodel.ReqContext, folderUIDs []string) ([]ngmodels.AlertRuleGroupWithFolderFullpath, error) {
folders, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.SignedInUser.GetOrgID(), c.SignedInUser) folders, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil { if err != nil {
return nil, err return nil, err
@ -165,13 +165,13 @@ func (srv RulerSrv) getRulesWithFolderTitleInFolders(c *contextmodel.ReqContext,
return nil, err return nil, err
} }
result := make([]ngmodels.AlertRuleGroupWithFolderTitle, 0, len(rulesByGroup)) result := make([]ngmodels.AlertRuleGroupWithFolderFullpath, 0, len(rulesByGroup))
for groupKey, rulesGroup := range rulesByGroup { for groupKey, rulesGroup := range rulesByGroup {
namespace, ok := folders[groupKey.NamespaceUID] namespace, ok := folders[groupKey.NamespaceUID]
if !ok { if !ok {
continue // user does not have access continue // user does not have access
} }
result = append(result, ngmodels.NewAlertRuleGroupWithFolderTitleFromRulesGroup(groupKey, rulesGroup, namespace.Title)) result = append(result, ngmodels.NewAlertRuleGroupWithFolderFullpathFromRulesGroup(groupKey, rulesGroup, namespace.Fullpath))
} }
return result, nil return result, nil
} }

View File

@ -31,8 +31,9 @@ var testData embed.FS
func TestExportFromPayload(t *testing.T) { func TestExportFromPayload(t *testing.T) {
orgID := int64(1) orgID := int64(1)
folder := &folder2.Folder{ folder := &folder2.Folder{
UID: "e4584834-1a87-4dff-8913-8a4748dfca79", UID: "e4584834-1a87-4dff-8913-8a4748dfca79",
Title: "foo bar", Title: "foo bar",
Fullpath: "foo bar",
} }
ruleStore := fakes.NewRuleStore(t) ruleStore := fakes.NewRuleStore(t)
@ -405,12 +406,12 @@ func TestExportRules(t *testing.T) {
if tc.expectedStatus != 200 { if tc.expectedStatus != 200 {
return return
} }
var exp []ngmodels.AlertRuleGroupWithFolderTitle var exp []ngmodels.AlertRuleGroupWithFolderFullpath
gr := ngmodels.GroupByAlertRuleGroupKey(tc.expectedRules) gr := ngmodels.GroupByAlertRuleGroupKey(tc.expectedRules)
for key, rules := range gr { for key, rules := range gr {
folder, err := ruleStore.GetNamespaceByUID(context.Background(), key.NamespaceUID, orgID, nil) folder, err := ruleStore.GetNamespaceByUID(context.Background(), key.NamespaceUID, orgID, nil)
require.NoError(t, err) require.NoError(t, err)
exp = append(exp, ngmodels.NewAlertRuleGroupWithFolderTitleFromRulesGroup(key, rules, folder.Title)) exp = append(exp, ngmodels.NewAlertRuleGroupWithFolderFullpathFromRulesGroup(key, rules, folder.Fullpath))
} }
sort.SliceStable(exp, func(i, j int) bool { sort.SliceStable(exp, func(i, j int) bool {
gi, gj := exp[i], exp[j] gi, gj := exp[i], exp[j]
@ -422,7 +423,7 @@ func TestExportRules(t *testing.T) {
} }
return gi.Title < gj.Title return gi.Title < gj.Title
}) })
groups, err := AlertingFileExportFromAlertRuleGroupWithFolderTitle(exp) groups, err := AlertingFileExportFromAlertRuleGroupWithFolderFullpath(exp)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, string(exportResponse(rc, groups).Body()), string(resp.Body())) require.Equal(t, string(exportResponse(rc, groups).Body()), string(resp.Body()))

View File

@ -135,11 +135,11 @@ func ApiAlertRuleGroupFromAlertRuleGroup(d models.AlertRuleGroup) definitions.Al
} }
} }
// AlertingFileExportFromAlertRuleGroupWithFolderTitle creates an definitions.AlertingFileExport DTO from []models.AlertRuleGroupWithFolderTitle. // AlertingFileExportFromAlertRuleGroupWithFolderFullpath creates an definitions.AlertingFileExport DTO from []models.AlertRuleGroupWithFolderTitle.
func AlertingFileExportFromAlertRuleGroupWithFolderTitle(groups []models.AlertRuleGroupWithFolderTitle) (definitions.AlertingFileExport, error) { func AlertingFileExportFromAlertRuleGroupWithFolderFullpath(groups []models.AlertRuleGroupWithFolderFullpath) (definitions.AlertingFileExport, error) {
f := definitions.AlertingFileExport{APIVersion: 1} f := definitions.AlertingFileExport{APIVersion: 1}
for _, group := range groups { for _, group := range groups {
export, err := AlertRuleGroupExportFromAlertRuleGroupWithFolderTitle(group) export, err := AlertRuleGroupExportFromAlertRuleGroupWithFolderFullpath(group)
if err != nil { if err != nil {
return definitions.AlertingFileExport{}, err return definitions.AlertingFileExport{}, err
} }
@ -148,8 +148,8 @@ func AlertingFileExportFromAlertRuleGroupWithFolderTitle(groups []models.AlertRu
return f, nil return f, nil
} }
// AlertRuleGroupExportFromAlertRuleGroupWithFolderTitle creates a definitions.AlertRuleGroupExport DTO from models.AlertRuleGroup. // AlertRuleGroupExportFromAlertRuleGroupWithFolderFullpath creates a definitions.AlertRuleGroupExport DTO from models.AlertRuleGroup.
func AlertRuleGroupExportFromAlertRuleGroupWithFolderTitle(d models.AlertRuleGroupWithFolderTitle) (definitions.AlertRuleGroupExport, error) { func AlertRuleGroupExportFromAlertRuleGroupWithFolderFullpath(d models.AlertRuleGroupWithFolderFullpath) (definitions.AlertRuleGroupExport, error) {
rules := make([]definitions.AlertRuleExport, 0, len(d.Rules)) rules := make([]definitions.AlertRuleExport, 0, len(d.Rules))
for i := range d.Rules { for i := range d.Rules {
alert, err := AlertRuleExportFromAlertRule(d.Rules[i]) alert, err := AlertRuleExportFromAlertRule(d.Rules[i])
@ -161,7 +161,7 @@ func AlertRuleGroupExportFromAlertRuleGroupWithFolderTitle(d models.AlertRuleGro
return definitions.AlertRuleGroupExport{ return definitions.AlertRuleGroupExport{
OrgID: d.OrgID, OrgID: d.OrgID,
Name: d.Title, Name: d.Title,
Folder: d.FolderTitle, Folder: d.FolderFullpath,
FolderUID: d.FolderUID, FolderUID: d.FolderUID,
Interval: model.Duration(time.Duration(d.Interval) * time.Second), Interval: model.Duration(time.Duration(d.Interval) * time.Second),
IntervalSeconds: d.Interval, IntervalSeconds: d.Interval,

View File

@ -185,42 +185,42 @@ type AlertRuleGroup struct {
Rules []AlertRule Rules []AlertRule
} }
// AlertRuleGroupWithFolderTitle extends AlertRuleGroup with orgID and folder title // AlertRuleGroupWithFolderFullpath extends AlertRuleGroup with orgID and folder title
type AlertRuleGroupWithFolderTitle struct { type AlertRuleGroupWithFolderFullpath struct {
*AlertRuleGroup *AlertRuleGroup
OrgID int64 OrgID int64
FolderTitle string FolderFullpath string
} }
func NewAlertRuleGroupWithFolderTitle(groupKey AlertRuleGroupKey, rules []AlertRule, folderTitle string) AlertRuleGroupWithFolderTitle { func NewAlertRuleGroupWithFolderFullpath(groupKey AlertRuleGroupKey, rules []AlertRule, folderFullpath string) AlertRuleGroupWithFolderFullpath {
SortAlertRulesByGroupIndex(rules) SortAlertRulesByGroupIndex(rules)
var interval int64 var interval int64
if len(rules) > 0 { if len(rules) > 0 {
interval = rules[0].IntervalSeconds interval = rules[0].IntervalSeconds
} }
var result = AlertRuleGroupWithFolderTitle{ var result = AlertRuleGroupWithFolderFullpath{
AlertRuleGroup: &AlertRuleGroup{ AlertRuleGroup: &AlertRuleGroup{
Title: groupKey.RuleGroup, Title: groupKey.RuleGroup,
FolderUID: groupKey.NamespaceUID, FolderUID: groupKey.NamespaceUID,
Interval: interval, Interval: interval,
Rules: rules, Rules: rules,
}, },
FolderTitle: folderTitle, FolderFullpath: folderFullpath,
OrgID: groupKey.OrgID, OrgID: groupKey.OrgID,
} }
return result return result
} }
func NewAlertRuleGroupWithFolderTitleFromRulesGroup(groupKey AlertRuleGroupKey, rules RulesGroup, folderTitle string) AlertRuleGroupWithFolderTitle { func NewAlertRuleGroupWithFolderFullpathFromRulesGroup(groupKey AlertRuleGroupKey, rules RulesGroup, folderFullpath string) AlertRuleGroupWithFolderFullpath {
derefRules := make([]AlertRule, 0, len(rules)) derefRules := make([]AlertRule, 0, len(rules))
for _, rule := range rules { for _, rule := range rules {
derefRules = append(derefRules, *rule) derefRules = append(derefRules, *rule)
} }
return NewAlertRuleGroupWithFolderTitle(groupKey, derefRules, folderTitle) return NewAlertRuleGroupWithFolderFullpath(groupKey, derefRules, folderFullpath)
} }
// SortAlertRuleGroupWithFolderTitle sorts AlertRuleGroupWithFolderTitle by folder UID and group name // SortAlertRuleGroupWithFolderTitle sorts AlertRuleGroupWithFolderTitle by folder UID and group name
func SortAlertRuleGroupWithFolderTitle(g []AlertRuleGroupWithFolderTitle) { func SortAlertRuleGroupWithFolderTitle(g []AlertRuleGroupWithFolderFullpath) {
sort.SliceStable(g, func(i, j int) bool { sort.SliceStable(g, func(i, j int) bool {
if g[i].AlertRuleGroup.FolderUID == g[j].AlertRuleGroup.FolderUID { if g[i].AlertRuleGroup.FolderUID == g[j].AlertRuleGroup.FolderUID {
return g[i].AlertRuleGroup.Title < g[j].AlertRuleGroup.Title return g[i].AlertRuleGroup.Title < g[j].AlertRuleGroup.Title

View File

@ -354,7 +354,7 @@ func (ng *AlertNG) init() error {
contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log, ng.store) contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log, ng.store)
templateService := provisioning.NewTemplateService(ng.store, 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) muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log)
alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.dashboardService, ng.QuotaService, ng.store, alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.QuotaService, ng.store,
int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()), int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit, ng.Log, notifier.NewNotificationSettingsValidationService(ng.store), ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit, ng.Log, notifier.NewNotificationSettingsValidationService(ng.store),

View File

@ -39,7 +39,6 @@ type AlertRuleService struct {
ruleStore RuleStore ruleStore RuleStore
provenanceStore ProvisioningStore provenanceStore ProvisioningStore
folderService folder.Service folderService folder.Service
dashboardService dashboards.DashboardService
quotas QuotaChecker quotas QuotaChecker
xact TransactionManager xact TransactionManager
log log.Logger log log.Logger
@ -50,7 +49,6 @@ type AlertRuleService struct {
func NewAlertRuleService(ruleStore RuleStore, func NewAlertRuleService(ruleStore RuleStore,
provenanceStore ProvisioningStore, provenanceStore ProvisioningStore,
folderService folder.Service, folderService folder.Service,
dashboardService dashboards.DashboardService,
quotas QuotaChecker, quotas QuotaChecker,
xact TransactionManager, xact TransactionManager,
defaultIntervalSeconds int64, defaultIntervalSeconds int64,
@ -67,7 +65,6 @@ func NewAlertRuleService(ruleStore RuleStore,
ruleStore: ruleStore, ruleStore: ruleStore,
provenanceStore: provenanceStore, provenanceStore: provenanceStore,
folderService: folderService, folderService: folderService,
dashboardService: dashboardService,
quotas: quotas, quotas: quotas,
xact: xact, xact: xact,
log: log, log: log,
@ -147,31 +144,33 @@ func (service *AlertRuleService) GetAlertRule(ctx context.Context, user identity
return rule, provenance, nil return rule, provenance, nil
} }
type AlertRuleWithFolderTitle struct { type AlertRuleWithFolderFullpath struct {
AlertRule models.AlertRule AlertRule models.AlertRule
FolderTitle string FolderFullpath string
} }
// GetAlertRuleWithFolderTitle returns a single alert rule with its folder title. // GetAlertRuleWithFolderFullpath returns a single alert rule with its folder title.
func (service *AlertRuleService) GetAlertRuleWithFolderTitle(ctx context.Context, user identity.Requester, ruleUID string) (AlertRuleWithFolderTitle, error) { func (service *AlertRuleService) GetAlertRuleWithFolderFullpath(ctx context.Context, user identity.Requester, ruleUID string) (AlertRuleWithFolderFullpath, error) {
rule, err := service.getAlertRuleAuthorized(ctx, user, ruleUID) rule, err := service.getAlertRuleAuthorized(ctx, user, ruleUID)
if err != nil { if err != nil {
return AlertRuleWithFolderTitle{}, err return AlertRuleWithFolderFullpath{}, err
} }
dq := dashboards.GetDashboardQuery{ fq := folder.GetFolderQuery{
OrgID: user.GetOrgID(), OrgID: user.GetOrgID(),
UID: rule.NamespaceUID, UID: &rule.NamespaceUID,
WithFullpath: true,
SignedInUser: user,
} }
dash, err := service.dashboardService.GetDashboard(ctx, &dq) f, err := service.folderService.Get(ctx, &fq)
if err != nil { if err != nil {
return AlertRuleWithFolderTitle{}, err return AlertRuleWithFolderFullpath{}, err
} }
return AlertRuleWithFolderTitle{ return AlertRuleWithFolderFullpath{
AlertRule: rule, AlertRule: rule,
FolderTitle: dash.Title, FolderFullpath: f.Fullpath,
}, nil }, nil
} }
@ -699,28 +698,30 @@ func (service *AlertRuleService) deleteRules(ctx context.Context, orgID int64, t
return nil return nil
} }
// GetAlertRuleGroupWithFolderTitle returns the alert rule group with folder title. // GetAlertRuleGroupWithFolderFullpath returns the alert rule group with folder title.
func (service *AlertRuleService) GetAlertRuleGroupWithFolderTitle(ctx context.Context, user identity.Requester, namespaceUID, group string) (models.AlertRuleGroupWithFolderTitle, error) { func (service *AlertRuleService) GetAlertRuleGroupWithFolderFullpath(ctx context.Context, user identity.Requester, namespaceUID, group string) (models.AlertRuleGroupWithFolderFullpath, error) {
ruleList, err := service.GetRuleGroup(ctx, user, namespaceUID, group) ruleList, err := service.GetRuleGroup(ctx, user, namespaceUID, group)
if err != nil { if err != nil {
return models.AlertRuleGroupWithFolderTitle{}, err return models.AlertRuleGroupWithFolderFullpath{}, err
} }
dq := dashboards.GetDashboardQuery{ fq := folder.GetFolderQuery{
OrgID: user.GetOrgID(), OrgID: user.GetOrgID(),
UID: namespaceUID, UID: &namespaceUID,
WithFullpath: true,
SignedInUser: user,
} }
dash, err := service.dashboardService.GetDashboard(ctx, &dq) f, err := service.folderService.Get(ctx, &fq)
if err != nil { if err != nil {
return models.AlertRuleGroupWithFolderTitle{}, err return models.AlertRuleGroupWithFolderFullpath{}, err
} }
res := models.NewAlertRuleGroupWithFolderTitle(ruleList.Rules[0].GetGroupKey(), ruleList.Rules, dash.Title) res := models.NewAlertRuleGroupWithFolderFullpath(ruleList.Rules[0].GetGroupKey(), ruleList.Rules, f.Fullpath)
return res, nil return res, nil
} }
// GetAlertGroupsWithFolderTitle returns all groups with folder title in the folders identified by folderUID that have at least one alert. If argument folderUIDs is nil or empty - returns groups in all folders. // GetAlertGroupsWithFolderFullpath returns all groups with folder's full path in the folders identified by folderUID that have at least one alert. If argument folderUIDs is nil or empty - returns groups in all folders.
func (service *AlertRuleService) GetAlertGroupsWithFolderTitle(ctx context.Context, user identity.Requester, folderUIDs []string) ([]models.AlertRuleGroupWithFolderTitle, error) { func (service *AlertRuleService) GetAlertGroupsWithFolderFullpath(ctx context.Context, user identity.Requester, folderUIDs []string) ([]models.AlertRuleGroupWithFolderFullpath, error) {
q := models.ListAlertRulesQuery{ q := models.ListAlertRulesQuery{
OrgID: user.GetOrgID(), OrgID: user.GetOrgID(),
} }
@ -758,33 +759,36 @@ func (service *AlertRuleService) GetAlertGroupsWithFolderTitle(ctx context.Conte
} }
if len(namespaces) == 0 { if len(namespaces) == 0 {
return []models.AlertRuleGroupWithFolderTitle{}, nil return []models.AlertRuleGroupWithFolderFullpath{}, nil
} }
dq := dashboards.GetDashboardsQuery{ fq := folder.GetFoldersQuery{
DashboardUIDs: nil, OrgID: user.GetOrgID(),
UIDs: nil,
WithFullpath: true,
SignedInUser: user,
} }
for uid := range namespaces { for uid := range namespaces {
dq.DashboardUIDs = append(dq.DashboardUIDs, uid) fq.UIDs = append(fq.UIDs, uid)
} }
// We need folder titles for the provisioning file format. We do it this way instead of using GetUserVisibleNamespaces to avoid folder:read permissions that should not apply to those with alert.provisioning:read. // We need folder titles for the provisioning file format. We do it this way instead of using GetUserVisibleNamespaces to avoid folder:read permissions that should not apply to those with alert.provisioning:read.
dashes, err := service.dashboardService.GetDashboards(ctx, &dq) folders, err := service.folderService.GetFolders(ctx, fq)
if err != nil { if err != nil {
return nil, err return nil, err
} }
folderUidToTitle := make(map[string]string) folderUidToFullpath := make(map[string]string)
for _, dash := range dashes { for _, folder := range folders {
folderUidToTitle[dash.UID] = dash.Title folderUidToFullpath[folder.UID] = folder.Fullpath
} }
result := make([]models.AlertRuleGroupWithFolderTitle, 0) result := make([]models.AlertRuleGroupWithFolderFullpath, 0)
for groupKey, rules := range groups { for groupKey, rules := range groups {
title, ok := folderUidToTitle[groupKey.NamespaceUID] fullpath, ok := folderUidToFullpath[groupKey.NamespaceUID]
if !ok { if !ok {
return nil, fmt.Errorf("cannot find title for folder with uid '%s'", groupKey.NamespaceUID) return nil, fmt.Errorf("cannot find full path for folder with uid '%s'", groupKey.NamespaceUID)
} }
result = append(result, models.NewAlertRuleGroupWithFolderTitleFromRulesGroup(groupKey, rules, title)) result = append(result, models.NewAlertRuleGroupWithFolderFullpathFromRulesGroup(groupKey, rules, fullpath))
} }
// Return results in a stable manner. // Return results in a stable manner.

View File

@ -13,24 +13,32 @@ import (
"github.com/stretchr/testify/assert" "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/expr" "github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder" "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/folder/foldertest"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/ngalert/testutil"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func TestAlertRuleService(t *testing.T) { func TestAlertRuleService(t *testing.T) {
ruleService := createAlertRuleService(t) ruleService := createAlertRuleService(t, nil)
var orgID int64 = 1 var orgID int64 = 1
u := &user.SignedInUser{ u := &user.SignedInUser{
UserID: 1, UserID: 1,
@ -82,7 +90,7 @@ func TestAlertRuleService(t *testing.T) {
}) })
t.Run("group update should propagate folderUID from group to rules", func(t *testing.T) { t.Run("group update should propagate folderUID from group to rules", func(t *testing.T) {
ruleService := createAlertRuleService(t) ruleService := createAlertRuleService(t, nil)
group := createDummyGroup("namespace-test", orgID) group := createDummyGroup("namespace-test", orgID)
group.Rules[0].NamespaceUID = "" group.Rules[0].NamespaceUID = ""
@ -523,7 +531,7 @@ func TestAlertRuleService(t *testing.T) {
}) })
t.Run("quota met causes create to be rejected", func(t *testing.T) { t.Run("quota met causes create to be rejected", func(t *testing.T) {
ruleService := createAlertRuleService(t) ruleService := createAlertRuleService(t, nil)
checker := &MockQuotaChecker{} checker := &MockQuotaChecker{}
checker.EXPECT().LimitExceeded() checker.EXPECT().LimitExceeded()
ruleService.quotas = checker ruleService.quotas = checker
@ -534,7 +542,7 @@ func TestAlertRuleService(t *testing.T) {
}) })
t.Run("quota met causes group write to be rejected", func(t *testing.T) { t.Run("quota met causes group write to be rejected", func(t *testing.T) {
ruleService := createAlertRuleService(t) ruleService := createAlertRuleService(t, nil)
checker := &MockQuotaChecker{} checker := &MockQuotaChecker{}
checker.EXPECT().LimitExceeded() checker.EXPECT().LimitExceeded()
ruleService.quotas = checker ruleService.quotas = checker
@ -757,7 +765,7 @@ func TestCreateAlertRule(t *testing.T) {
}) })
}) })
ruleService := createAlertRuleService(t) ruleService := createAlertRuleService(t, nil)
t.Run("should return the created id", func(t *testing.T) { t.Run("should return the created id", func(t *testing.T) {
rule, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#1", orgID), models.ProvenanceNone) rule, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#1", orgID), models.ProvenanceNone)
require.NoError(t, err) require.NoError(t, err)
@ -1463,6 +1471,88 @@ func TestDeleteRuleGroup(t *testing.T) {
}) })
} }
func TestProvisiongWithFullpath(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
inProcBus := bus.ProvideBus(tracer)
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
_, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg)
ac := acmock.New()
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
folderService := folderimpl.ProvideService(ac, inProcBus, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil)
ruleService := createAlertRuleService(t, folderService)
var orgID int64 = 1
signedInUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
orgID: {
dashboards.ActionFoldersCreate: {},
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll},
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
}}
namespaceUID := "my-namespace"
namespaceTitle := namespaceUID
rootFolder, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{
UID: namespaceUID,
Title: namespaceTitle,
OrgID: orgID,
SignedInUser: &signedInUser,
})
require.NoError(t, err)
t.Run("for a rule under a root folder should set the right fullpath", func(t *testing.T) {
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), []models.AlertRule{
createTestRule("my-cool-group", "my-cool-group", orgID, namespaceUID),
})
require.NoError(t, err)
require.Len(t, r, 1)
res, err := ruleService.GetAlertRuleWithFolderFullpath(context.Background(), &signedInUser, r[0].UID)
require.NoError(t, err)
assert.Equal(t, namespaceTitle, res.FolderFullpath)
res2, err := ruleService.GetAlertRuleGroupWithFolderFullpath(context.Background(), &signedInUser, namespaceUID, "my-cool-group")
require.NoError(t, err)
assert.Equal(t, namespaceTitle, res2.FolderFullpath)
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, []string{namespaceUID})
require.NoError(t, err)
assert.Equal(t, namespaceTitle, res3[0].FolderFullpath)
})
t.Run("for a rule under a subfolder should set the right fullpath", func(t *testing.T) {
otherNamespaceUID := "my-other-namespace"
otherNamespaceTitle := "my-other-namespace containing multiple //"
_, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{
UID: otherNamespaceUID,
Title: otherNamespaceTitle,
OrgID: orgID,
ParentUID: rootFolder.UID,
SignedInUser: &signedInUser,
})
require.NoError(t, err)
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), []models.AlertRule{
createTestRule("my-cool-group-2", "my-cool-group-2", orgID, otherNamespaceUID),
})
require.NoError(t, err)
require.Len(t, r, 1)
res, err := ruleService.GetAlertRuleWithFolderFullpath(context.Background(), &signedInUser, r[0].UID)
require.NoError(t, err)
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res.FolderFullpath)
res2, err := ruleService.GetAlertRuleGroupWithFolderFullpath(context.Background(), &signedInUser, otherNamespaceUID, "my-cool-group-2")
require.NoError(t, err)
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res2.FolderFullpath)
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, []string{otherNamespaceUID})
require.NoError(t, err)
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res3[0].FolderFullpath)
})
}
func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery { func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
generic := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) { generic := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
a, ok := cmd.(fakes.GenericRecordedQuery) a, ok := cmd.(fakes.GenericRecordedQuery)
@ -1478,13 +1568,9 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
return result return result
} }
func createAlertRuleService(t *testing.T) AlertRuleService { func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService {
t.Helper() t.Helper()
sqlStore := db.InitTestDB(t) sqlStore := db.InitTestDB(t)
folderService := foldertest.NewFakeService()
folderService.ExpectedFolder = &folder.Folder{
UID: "default-namespace",
}
store := store.DBstore{ store := store.DBstore{
SQLStore: sqlStore, SQLStore: sqlStore,
Cfg: setting.UnifiedAlertingSettings{ Cfg: setting.UnifiedAlertingSettings{
@ -1496,6 +1582,11 @@ func createAlertRuleService(t *testing.T) AlertRuleService {
// store := fakes.NewRuleStore(t) // store := fakes.NewRuleStore(t)
quotas := MockQuotaChecker{} quotas := MockQuotaChecker{}
quotas.EXPECT().LimitOK() quotas.EXPECT().LimitOK()
if folderService == nil {
folderService = foldertest.NewFakeService()
}
return AlertRuleService{ return AlertRuleService{
ruleStore: store, ruleStore: store,
provenanceStore: store, provenanceStore: store,

View File

@ -6,12 +6,13 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/provisioning"
) )
type ProvisionerConfig struct { type ProvisionerConfig struct {
Path string Path string
DashboardService dashboards.DashboardService FolderService folder.Service
DashboardProvService dashboards.DashboardProvisioningService DashboardProvService dashboards.DashboardProvisioningService
RuleService provisioning.AlertRuleService RuleService provisioning.AlertRuleService
ContactPointService provisioning.ContactPointService ContactPointService provisioning.ContactPointService
@ -63,7 +64,7 @@ func Provision(ctx context.Context, cfg ProvisionerConfig) error {
} }
ruleProvisioner := NewAlertRuleProvisioner( ruleProvisioner := NewAlertRuleProvisioner(
logger, logger,
cfg.DashboardService, cfg.FolderService,
cfg.DashboardProvService, cfg.DashboardProvService,
cfg.RuleService) cfg.RuleService)
err = ruleProvisioner.Provision(ctx, files) err = ruleProvisioner.Provision(ctx, files)

View File

@ -6,11 +6,11 @@ import (
"fmt" "fmt"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
alert_models "github.com/grafana/grafana/pkg/services/ngalert/models" alert_models "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@ -23,12 +23,12 @@ type AlertRuleProvisioner interface {
func NewAlertRuleProvisioner( func NewAlertRuleProvisioner(
logger log.Logger, logger log.Logger,
dashboardService dashboards.DashboardService, folderService folder.Service,
dashboardProvService dashboards.DashboardProvisioningService, dashboardProvService dashboards.DashboardProvisioningService,
ruleService provisioning.AlertRuleService) AlertRuleProvisioner { ruleService provisioning.AlertRuleService) AlertRuleProvisioner {
return &defaultAlertRuleProvisioner{ return &defaultAlertRuleProvisioner{
logger: logger, logger: logger,
dashboardService: dashboardService, folderService: folderService,
dashboardProvService: dashboardProvService, dashboardProvService: dashboardProvService,
ruleService: ruleService, ruleService: ruleService,
} }
@ -36,7 +36,7 @@ func NewAlertRuleProvisioner(
type defaultAlertRuleProvisioner struct { type defaultAlertRuleProvisioner struct {
logger log.Logger logger log.Logger
dashboardService dashboards.DashboardService folderService folder.Service
dashboardProvService dashboards.DashboardProvisioningService dashboardProvService dashboards.DashboardProvisioningService
ruleService provisioning.AlertRuleService ruleService provisioning.AlertRuleService
} }
@ -46,13 +46,14 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
for _, file := range files { for _, file := range files {
for _, group := range file.Groups { for _, group := range file.Groups {
u := provisionerUser(group.OrgID) u := provisionerUser(group.OrgID)
folderUID, err := prov.getOrCreateFolderUID(ctx, group.FolderTitle, group.OrgID) folderUID, err := prov.getOrCreateFolderFullpath(ctx, group.FolderFullpath, group.OrgID)
if err != nil { if err != nil {
prov.logger.Error("failed to get or create folder", "folder", group.FolderFullpath, "org", group.OrgID, "err", err)
return err return err
} }
prov.logger.Debug("provisioning alert rule group", prov.logger.Debug("provisioning alert rule group",
"org", group.OrgID, "org", group.OrgID,
"folder", group.FolderTitle, "folder", group.FolderFullpath,
"folderUID", folderUID, "folderUID", folderUID,
"name", group.Title) "name", group.Title)
for _, rule := range group.Rules { for _, rule := range group.Rules {
@ -98,36 +99,57 @@ func (prov *defaultAlertRuleProvisioner) provisionRule(
return err return err
} }
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( func (prov *defaultAlertRuleProvisioner) getOrCreateFolderFullpath(
ctx context.Context, folderName string, orgID int64) (string, error) { ctx context.Context, folderFullpath string, orgID int64) (string, error) {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc() folderTitles := folderimpl.SplitFullpath(folderFullpath)
cmd := &dashboards.GetDashboardQuery{ if len(folderTitles) == 0 {
Title: &folderName, return "", fmt.Errorf("invalid folder fullpath: %s", folderFullpath)
FolderID: util.Pointer(int64(0)), // nolint:staticcheck
OrgID: orgID,
} }
cmdResult, err := prov.dashboardService.GetDashboard(ctx, cmd)
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { var folderUID *string
for i := range folderTitles {
uid, err := prov.getOrCreateFolderByTitle(ctx, folderTitles[i], orgID, folderUID)
if err != nil {
prov.logger.Error("failed to get or create folder", "folder", folderTitles[i], "org", orgID, "err", err)
return "", err
}
folderUID = &uid
}
return *folderUID, nil
}
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderByTitle(
ctx context.Context, folderName string, orgID int64, parentUID *string) (string, error) {
cmd := &folder.GetFolderQuery{
Title: &folderName,
ParentUID: parentUID,
OrgID: orgID,
SignedInUser: provisionerUser(orgID),
}
cmdResult, err := prov.folderService.Get(ctx, cmd)
if err != nil && !errors.Is(err, dashboards.ErrFolderNotFound) {
return "", err return "", err
} }
// dashboard folder not found. create one. // dashboard folder not found. create one.
if errors.Is(err, dashboards.ErrDashboardNotFound) { if errors.Is(err, dashboards.ErrFolderNotFound) {
createCmd := &folder.CreateFolderCommand{ createCmd := &folder.CreateFolderCommand{
OrgID: orgID, OrgID: orgID,
UID: util.GenerateShortUID(), UID: util.GenerateShortUID(),
Title: folderName, Title: folderName,
} }
dbDash, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, createCmd)
if parentUID != nil {
createCmd.ParentUID = *parentUID
}
f, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, createCmd)
if err != nil { if err != nil {
return "", err return "", err
} }
return dbDash.UID, nil return f.UID, nil
}
if !cmdResult.IsFolder {
return "", fmt.Errorf("got invalid response. expected folder, found dashboard")
} }
return cmdResult.UID, nil return cmdResult.UID, nil

View File

@ -32,11 +32,11 @@ type AlertRuleGroupV1 struct {
Rules []AlertRuleV1 `json:"rules" yaml:"rules"` Rules []AlertRuleV1 `json:"rules" yaml:"rules"`
} }
func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (models.AlertRuleGroupWithFolderTitle, error) { func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (models.AlertRuleGroupWithFolderFullpath, error) {
ruleGroup := models.AlertRuleGroupWithFolderTitle{AlertRuleGroup: &models.AlertRuleGroup{}} ruleGroup := models.AlertRuleGroupWithFolderFullpath{AlertRuleGroup: &models.AlertRuleGroup{}}
ruleGroup.Title = ruleGroupV1.Name.Value() ruleGroup.Title = ruleGroupV1.Name.Value()
if strings.TrimSpace(ruleGroup.Title) == "" { if strings.TrimSpace(ruleGroup.Title) == "" {
return models.AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no name set") return models.AlertRuleGroupWithFolderFullpath{}, errors.New("rule group has no name set")
} }
ruleGroup.OrgID = ruleGroupV1.OrgID.Value() ruleGroup.OrgID = ruleGroupV1.OrgID.Value()
if ruleGroup.OrgID < 1 { if ruleGroup.OrgID < 1 {
@ -44,17 +44,17 @@ func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (models.AlertRuleGroupWithFold
} }
interval, err := model.ParseDuration(ruleGroupV1.Interval.Value()) interval, err := model.ParseDuration(ruleGroupV1.Interval.Value())
if err != nil { if err != nil {
return models.AlertRuleGroupWithFolderTitle{}, err return models.AlertRuleGroupWithFolderFullpath{}, err
} }
ruleGroup.Interval = int64(time.Duration(interval).Seconds()) ruleGroup.Interval = int64(time.Duration(interval).Seconds())
ruleGroup.FolderTitle = ruleGroupV1.Folder.Value() ruleGroup.FolderFullpath = ruleGroupV1.Folder.Value()
if strings.TrimSpace(ruleGroup.FolderTitle) == "" { if strings.TrimSpace(ruleGroup.FolderFullpath) == "" {
return models.AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no folder set") return models.AlertRuleGroupWithFolderFullpath{}, errors.New("rule group has no folder set")
} }
for _, ruleV1 := range ruleGroupV1.Rules { for _, ruleV1 := range ruleGroupV1.Rules {
rule, err := ruleV1.mapToModel(ruleGroup.OrgID) rule, err := ruleV1.mapToModel(ruleGroup.OrgID)
if err != nil { if err != nil {
return models.AlertRuleGroupWithFolderTitle{}, err return models.AlertRuleGroupWithFolderFullpath{}, err
} }
ruleGroup.Rules = append(ruleGroup.Rules, rule) ruleGroup.Rules = append(ruleGroup.Rules, rule)
} }

View File

@ -16,7 +16,7 @@ type OrgID int64
type AlertingFile struct { type AlertingFile struct {
configVersion configVersion
Filename string Filename string
Groups []models.AlertRuleGroupWithFolderTitle Groups []models.AlertRuleGroupWithFolderFullpath
DeleteRules []RuleDelete DeleteRules []RuleDelete
ContactPoints []ContactPoint ContactPoints []ContactPoint
DeleteContactPoints []DeleteContactPoint DeleteContactPoints []DeleteContactPoint

View File

@ -337,6 +337,7 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic
return 0, "", ErrFolderNameMissing return 0, "", ErrFolderNameMissing
} }
// TODO use folder service instead
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc() metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc()
cmd := &dashboards.GetDashboardQuery{ cmd := &dashboards.GetDashboardQuery{
FolderID: util.Pointer(int64(0)), // nolint:staticcheck FolderID: util.Pointer(int64(0)), // nolint:staticcheck

View File

@ -114,7 +114,7 @@ func TestDashboardFileReader(t *testing.T) {
cfg.Folder = "Team A" cfg.Folder = "Team A"
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Once() fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{ID: 1}, nil).Once()
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 2}, nil).Times(2) fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 2}, nil).Times(2)
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil)
@ -380,7 +380,7 @@ func TestDashboardFileReader(t *testing.T) {
"folder": defaultDashboards, "folder": defaultDashboards,
}, },
} }
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Once() fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&folder.Folder{ID: 1}, nil).Once()
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil) r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, nil)
require.NoError(t, err) require.NoError(t, err)

View File

@ -259,8 +259,8 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
ruleService := provisioning.NewAlertRuleService( ruleService := provisioning.NewAlertRuleService(
st, st,
st, st,
nil, ps.folderService,
ps.dashboardService, //ps.dashboardService,
ps.quotaService, ps.quotaService,
ps.SQLStore, ps.SQLStore,
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
@ -280,7 +280,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
cfg := prov_alerting.ProvisionerConfig{ cfg := prov_alerting.ProvisionerConfig{
Path: alertingPath, Path: alertingPath,
RuleService: *ruleService, RuleService: *ruleService,
DashboardService: ps.dashboardService, FolderService: ps.folderService,
DashboardProvService: ps.dashboardProvisioningService, DashboardProvService: ps.dashboardProvisioningService,
ContactPointService: *contactPointService, ContactPointService: *contactPointService,
NotificiationPolicyService: *notificationPolicyService, NotificiationPolicyService: *notificationPolicyService,

View File

@ -509,7 +509,7 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
require.Equal(t, "folder1", allExport.Groups[0].Folder) require.Equal(t, "folder1", allExport.Groups[0].Folder)
require.Equal(t, "folder2", allExport.Groups[1].Folder) require.Equal(t, "folder2", allExport.Groups[1].Folder)
require.Equal(t, "subfolder", allExport.Groups[2].Folder) require.Equal(t, "folder1/subfolder", allExport.Groups[2].Folder)
}) })
t.Run("Export from one folder", func(t *testing.T) { t.Run("Export from one folder", func(t *testing.T) {
@ -632,7 +632,7 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
require.Equal(t, http.StatusOK, status) require.Equal(t, http.StatusOK, status)
require.Len(t, export.Groups, 2) require.Len(t, export.Groups, 2)
require.Equal(t, "folder1", export.Groups[0].Folder) require.Equal(t, "folder1", export.Groups[0].Folder)
require.Equal(t, "subfolder", export.Groups[1].Folder) require.Equal(t, "folder1/subfolder", export.Groups[1].Folder)
}) })
t.Run("Export from one folder", func(t *testing.T) { t.Run("Export from one folder", func(t *testing.T) {