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 215 additions and 220 deletions
+1
View File
@@ -129,6 +129,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
featureManager: api.FeatureManager,
appUrl: api.AppUrl,
tracer: api.Tracer,
folderService: api.RuleStore,
}), m)
api.RegisterConfigurationApiEndpoints(NewConfiguration(
&ConfigSrv{
+1 -1
View File
@@ -304,7 +304,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder
newGroup := &apimodels.RuleGroup{
Name: groupKey.RuleGroup,
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana.
File: ngmodels.GetNamespaceKey(folder.ParentUID, folder.Title),
File: folder.Fullpath,
}
rulesTotals := make(map[string]int64, len(rules))
+2 -6
View File
@@ -162,9 +162,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam
result := apimodels.NamespaceConfigResponse{}
for groupKey, rules := range ruleGroups {
key := ngmodels.GetNamespaceKey(namespace.ParentUID, namespace.Title)
// nolint:staticcheck
result[key] = append(result[key], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords))
result[namespace.Fullpath] = append(result[namespace.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords))
}
return response.JSON(http.StatusAccepted, result)
@@ -242,9 +240,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res
srv.log.Error("Namespace not visible to the user", "user", id, "userNamespace", userNamespace, "namespace", groupKey.NamespaceUID)
continue
}
key := ngmodels.GetNamespaceKey(folder.ParentUID, folder.Title)
// nolint:staticcheck
result[key] = append(result[key], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords))
result[folder.Fullpath] = append(result[folder.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords))
}
return response.JSON(http.StatusOK, result)
}
+6 -6
View File
@@ -200,7 +200,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
require.NoError(t, json.Unmarshal(response.Body(), result))
require.NotNil(t, result)
for namespace, groups := range *result {
require.Equal(t, models.GetNamespaceKey(folder.ParentUID, folder.Title), namespace)
require.Equal(t, folder.Fullpath, namespace)
for _, group := range groups {
grouploop:
for _, actualRule := range group.Rules {
@@ -243,7 +243,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
require.NotNil(t, result)
found := false
for namespace, groups := range *result {
require.Equal(t, models.GetNamespaceKey(folder.ParentUID, folder.Title), namespace)
require.Equal(t, folder.Fullpath, namespace)
for _, group := range groups {
for _, actualRule := range group.Rules {
if actualRule.GrafanaManagedAlert.UID == expectedRules[0].UID {
@@ -278,7 +278,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
models.RulesGroup(expectedRules).SortByGroupIndex()
groups, ok := (*result)[models.GetNamespaceKey(folder.ParentUID, folder.Title)]
groups, ok := (*result)[folder.Fullpath]
require.True(t, ok)
require.Len(t, groups, 1)
group := groups[0]
@@ -329,10 +329,10 @@ func TestRouteGetRulesConfig(t *testing.T) {
require.NoError(t, json.Unmarshal(response.Body(), result))
require.NotNil(t, result)
require.Contains(t, *result, models.GetNamespaceKey(folder1.ParentUID, folder1.Title))
require.Contains(t, *result, folder1.Fullpath)
require.NotContains(t, *result, folder2.UID)
groups := (*result)[models.GetNamespaceKey(folder1.ParentUID, folder1.Title)]
groups := (*result)[folder1.Fullpath]
require.Len(t, groups, 1)
require.Equal(t, group1Key.RuleGroup, groups[0].Name)
require.Len(t, groups[0].Rules, len(group1))
@@ -361,7 +361,7 @@ func TestRouteGetRulesConfig(t *testing.T) {
models.RulesGroup(expectedRules).SortByGroupIndex()
groups, ok := (*result)[models.GetNamespaceKey(folder.ParentUID, folder.Title)]
groups, ok := (*result)[folder.Fullpath]
require.True(t, ok)
require.Len(t, groups, 1)
group := groups[0]
@@ -2,6 +2,7 @@ package api
import (
"fmt"
"path"
"strconv"
"testing"
"time"
@@ -84,9 +85,10 @@ func validGroup(cfg *setting.UnifiedAlertingSettings, rules ...apimodels.Postabl
}
func randFolder() *folder.Folder {
title := "TEST-FOLDER-" + util.GenerateShortUID()
return &folder.Folder{
UID: util.GenerateShortUID(),
Title: "TEST-FOLDER-" + util.GenerateShortUID(),
Title: title,
// URL: "",
// Version: 0,
Created: time.Time{},
@@ -94,6 +96,8 @@ func randFolder() *folder.Folder {
// UpdatedBy: 0,
// CreatedBy: 0,
// HasACL: false,
ParentUID: uuid.NewString(),
Fullpath: path.Join("parent-folder", title),
}
}
+14 -7
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"errors"
"net/http"
"net/url"
@@ -18,7 +19,9 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
@@ -32,6 +35,10 @@ import (
"github.com/grafana/grafana/pkg/util"
)
type folderService interface {
GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error)
}
type TestingApiSrv struct {
*AlertingProxy
DatasourceCache datasources.CacheService
@@ -43,22 +50,23 @@ type TestingApiSrv struct {
featureManager featuremgmt.FeatureToggles
appUrl *url.URL
tracer tracing.Tracer
folderService folderService
}
// RouteTestGrafanaRuleConfig returns a list of potential alerts for a given rule configuration. This is intended to be
// as true as possible to what would be generated by the ruler except that the resulting alerts are not filtered to
// only Resolved / Firing and ready to send.
func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext, body apimodels.PostableExtendedRuleNodeExtended) response.Response {
folder, err := srv.folderService.GetNamespaceByUID(c.Req.Context(), body.NamespaceUID, c.OrgID, c.SignedInUser)
if err != nil {
return toNamespaceErrorResponse(dashboards.ErrFolderAccessDenied)
}
rule, err := validateRuleNode(
&body.Rule,
body.RuleGroup,
srv.cfg.BaseInterval,
c.SignedInUser.GetOrgID(),
&folder.Folder{
OrgID: c.SignedInUser.GetOrgID(),
UID: body.NamespaceUID,
Title: body.NamespaceTitle,
},
folder,
srv.cfg,
)
if err != nil {
@@ -103,8 +111,7 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext,
now,
rule,
results,
// TODO remove when switched to full path https://github.com/grafana/grafana/issues/80324
state.GetRuleExtraLabels(rule, ngmodels.GetNamespaceKey("", body.NamespaceTitle), includeFolder),
state.GetRuleExtraLabels(rule, folder.Fullpath, includeFolder),
)
alerts := make([]*amv2.PostableAlert, 0, len(transitions))
+55 -9
View File
@@ -6,6 +6,7 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/mock"
@@ -15,14 +16,17 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
fakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/ngalert/eval/eval_mocks"
"github.com/grafana/grafana/pkg/services/ngalert/models"
fakes2 "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web"
)
@@ -139,6 +143,36 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
},
}
t.Run("should return Forbidden if user cannot access folder", func(t *testing.T) {
ac := acMock.New().WithPermissions([]ac.Permission{
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceAllScope()},
})
ruleStore := fakes2.NewRuleStore(t)
ruleStore.Hook = func(cmd any) error {
q, ok := cmd.(fakes2.GenericRecordedQuery)
if !ok {
return nil
}
if q.Name == "GetNamespaceByUID" {
return dashboards.ErrFolderAccessDenied
}
return nil
}
srv := createTestingApiSrv(t, nil, ac, eval_mocks.NewEvaluatorFactory(&eval_mocks.ConditionEvaluatorMock{}), &featuremgmt.FeatureManager{}, ruleStore)
rule := validRule()
response := srv.RouteTestGrafanaRuleConfig(rc, definitions.PostableExtendedRuleNodeExtended{
Rule: rule,
NamespaceUID: uuid.NewString(),
NamespaceTitle: "test-folder",
})
require.Equal(t, http.StatusForbidden, response.Status())
})
t.Run("should return Forbidden if user cannot query a data source", func(t *testing.T) {
data1 := models.GenerateAlertQuery()
data2 := models.GenerateAlertQuery()
@@ -147,15 +181,18 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)},
})
srv := createTestingApiSrv(t, nil, ac, eval_mocks.NewEvaluatorFactory(&eval_mocks.ConditionEvaluatorMock{}), &featuremgmt.FeatureManager{})
f := randFolder()
ruleStore := fakes2.NewRuleStore(t)
ruleStore.Folders[rc.OrgID] = []*folder.Folder{f}
srv := createTestingApiSrv(t, nil, ac, eval_mocks.NewEvaluatorFactory(&eval_mocks.ConditionEvaluatorMock{}), &featuremgmt.FeatureManager{}, ruleStore)
rule := validRule()
rule.GrafanaManagedAlert.Data = ApiAlertQueriesFromAlertQueries([]models.AlertQuery{data1, data2})
rule.GrafanaManagedAlert.Condition = data2.RefID
response := srv.RouteTestGrafanaRuleConfig(rc, definitions.PostableExtendedRuleNodeExtended{
Rule: rule,
NamespaceUID: "test-folder",
NamespaceTitle: "test-folder",
NamespaceUID: f.UID,
NamespaceTitle: f.Title,
})
require.Equal(t, http.StatusForbidden, response.Status())
@@ -181,15 +218,19 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
evalFactory := eval_mocks.NewEvaluatorFactory(evaluator)
srv := createTestingApiSrv(t, ds, ac, evalFactory, &featuremgmt.FeatureManager{})
f := randFolder()
ruleStore := fakes2.NewRuleStore(t)
ruleStore.Folders[rc.OrgID] = []*folder.Folder{f}
srv := createTestingApiSrv(t, ds, ac, evalFactory, &featuremgmt.FeatureManager{}, ruleStore)
rule := validRule()
rule.GrafanaManagedAlert.Data = ApiAlertQueriesFromAlertQueries([]models.AlertQuery{data1, data2})
rule.GrafanaManagedAlert.Condition = data2.RefID
response := srv.RouteTestGrafanaRuleConfig(rc, definitions.PostableExtendedRuleNodeExtended{
Rule: rule,
NamespaceUID: "test-folder",
NamespaceTitle: "test-folder",
NamespaceUID: f.UID,
NamespaceTitle: f.Title,
})
require.Equal(t, http.StatusOK, response.Status())
@@ -256,7 +297,9 @@ func TestRouteEvalQueries(t *testing.T) {
}
evaluator.EXPECT().EvaluateRaw(mock.Anything, mock.Anything).Return(result, nil)
srv := createTestingApiSrv(t, ds, ac, eval_mocks.NewEvaluatorFactory(evaluator), &featuremgmt.FeatureManager{})
ruleStore := fakes2.NewRuleStore(t)
srv := createTestingApiSrv(t, ds, ac, eval_mocks.NewEvaluatorFactory(evaluator), &featuremgmt.FeatureManager{}, ruleStore)
response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{
Data: ApiAlertQueriesFromAlertQueries([]models.AlertQuery{data1, data2}),
@@ -316,7 +359,9 @@ func TestRouteEvalQueries(t *testing.T) {
}
evaluator.EXPECT().EvaluateRaw(mock.Anything, mock.Anything).Return(result, nil)
srv := createTestingApiSrv(t, ds, ac, eval_mocks.NewEvaluatorFactory(evaluator), featuremgmt.WithManager(featuremgmt.FlagAlertingQueryOptimization))
ruleStore := fakes2.NewRuleStore(t)
srv := createTestingApiSrv(t, ds, ac, eval_mocks.NewEvaluatorFactory(evaluator), featuremgmt.WithManager(featuremgmt.FlagAlertingQueryOptimization), ruleStore)
response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{
Data: ApiAlertQueriesFromAlertQueries(queries),
@@ -342,7 +387,7 @@ func TestRouteEvalQueries(t *testing.T) {
})
}
func createTestingApiSrv(t *testing.T, ds *fakes.FakeCacheService, ac *acMock.Mock, evaluator eval.EvaluatorFactory, featureManager *featuremgmt.FeatureManager) *TestingApiSrv {
func createTestingApiSrv(t *testing.T, ds *fakes.FakeCacheService, ac *acMock.Mock, evaluator eval.EvaluatorFactory, featureManager *featuremgmt.FeatureManager, ruleStore RuleStore) *TestingApiSrv {
if ac == nil {
ac = acMock.New()
}
@@ -354,5 +399,6 @@ func createTestingApiSrv(t *testing.T, ds *fakes.FakeCacheService, ac *acMock.Mo
cfg: config(t),
tracer: tracing.InitializeTracerForTest(),
featureManager: featureManager,
folderService: ruleStore,
}
}