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:
@@ -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{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user