Alerting: Improve validation of query and expressions on rule submit (#53258)
* Improve error messages of server-side expression * move validation of alert queries and a condition to eval package
This commit is contained in:
@@ -92,6 +92,8 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
||||
ac: api.AccessControl,
|
||||
}
|
||||
|
||||
evaluator := eval.NewEvaluator(api.Cfg, log.New("ngalert.eval"), api.DatasourceCache, api.ExpressionService)
|
||||
|
||||
// Register endpoints for proxying to Alertmanager-compatible backends.
|
||||
api.RegisterAlertmanagerApiEndpoints(NewForkingAM(
|
||||
api.DatasourceCache,
|
||||
@@ -109,15 +111,15 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
||||
api.DatasourceCache,
|
||||
NewLotexRuler(proxy, logger),
|
||||
&RulerSrv{
|
||||
DatasourceCache: api.DatasourceCache,
|
||||
QuotaService: api.QuotaService,
|
||||
scheduleService: api.Schedule,
|
||||
store: api.RuleStore,
|
||||
provenanceStore: api.ProvenanceStore,
|
||||
xactManager: api.TransactionManager,
|
||||
log: logger,
|
||||
cfg: &api.Cfg.UnifiedAlerting,
|
||||
ac: api.AccessControl,
|
||||
conditionValidator: evaluator,
|
||||
QuotaService: api.QuotaService,
|
||||
scheduleService: api.Schedule,
|
||||
store: api.RuleStore,
|
||||
provenanceStore: api.ProvenanceStore,
|
||||
xactManager: api.TransactionManager,
|
||||
log: logger,
|
||||
cfg: &api.Cfg.UnifiedAlerting,
|
||||
ac: api.AccessControl,
|
||||
},
|
||||
), m)
|
||||
api.RegisterTestingApiEndpoints(NewTestingApi(
|
||||
@@ -126,7 +128,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
||||
DatasourceCache: api.DatasourceCache,
|
||||
log: logger,
|
||||
accessControl: api.AccessControl,
|
||||
evaluator: eval.NewEvaluator(api.Cfg, log.New("ngalert.eval"), api.DatasourceCache, api.ExpressionService),
|
||||
evaluator: evaluator,
|
||||
}), m)
|
||||
api.RegisterConfigurationApiEndpoints(NewConfiguration(
|
||||
&ConfigSrv{
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
@@ -28,16 +28,21 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type ConditionValidator interface {
|
||||
// Validate validates that the condition is correct. Returns nil if the condition is correct. Otherwise, error that describes the failure
|
||||
Validate(ctx context.Context, user *user.SignedInUser, condition ngmodels.Condition) error
|
||||
}
|
||||
|
||||
type RulerSrv struct {
|
||||
xactManager provisioning.TransactionManager
|
||||
provenanceStore provisioning.ProvisioningStore
|
||||
store store.RuleStore
|
||||
DatasourceCache datasources.CacheService
|
||||
QuotaService quota.Service
|
||||
scheduleService schedule.ScheduleService
|
||||
log log.Logger
|
||||
cfg *setting.UnifiedAlertingSettings
|
||||
ac accesscontrol.AccessControl
|
||||
xactManager provisioning.TransactionManager
|
||||
provenanceStore provisioning.ProvisioningStore
|
||||
store store.RuleStore
|
||||
QuotaService quota.Service
|
||||
scheduleService schedule.ScheduleService
|
||||
log log.Logger
|
||||
cfg *setting.UnifiedAlertingSettings
|
||||
ac accesscontrol.AccessControl
|
||||
conditionValidator ConditionValidator
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -302,7 +307,9 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
|
||||
return toNamespaceErrorResponse(err)
|
||||
}
|
||||
|
||||
rules, err := validateRuleGroup(&ruleGroupConfig, c.SignedInUser.OrgID, namespace, conditionValidator(c, srv.DatasourceCache), srv.cfg)
|
||||
rules, err := validateRuleGroup(&ruleGroupConfig, c.SignedInUser.OrgID, namespace, func(condition ngmodels.Condition) error {
|
||||
return srv.conditionValidator.Validate(c.Req.Context(), c.SignedInUser, condition)
|
||||
}, srv.cfg)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
|
||||
@@ -646,7 +646,6 @@ func createService(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedu
|
||||
return &RulerSrv{
|
||||
xactManager: store,
|
||||
store: store,
|
||||
DatasourceCache: nil,
|
||||
QuotaService: nil,
|
||||
provenanceStore: provisioning.NewFakeProvisioningStore(),
|
||||
scheduleService: scheduler,
|
||||
|
||||
@@ -88,7 +88,6 @@ func validateRuleNode(
|
||||
if len(ruleNode.GrafanaManagedAlert.Data) != 0 {
|
||||
cond := ngmodels.Condition{
|
||||
Condition: ruleNode.GrafanaManagedAlert.Condition,
|
||||
OrgID: orgId,
|
||||
Data: ruleNode.GrafanaManagedAlert.Data,
|
||||
}
|
||||
if err := conditionValidator(cond); err != nil {
|
||||
|
||||
@@ -40,11 +40,10 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *models.ReqContext, body a
|
||||
|
||||
evalCond := ngmodels.Condition{
|
||||
Condition: body.GrafanaManagedCondition.Condition,
|
||||
OrgID: c.SignedInUser.OrgID,
|
||||
Data: body.GrafanaManagedCondition.Data,
|
||||
}
|
||||
|
||||
if err := validateCondition(c.Req.Context(), evalCond, c.SignedInUser, c.SkipCache, srv.DatasourceCache); err != nil {
|
||||
if err := srv.evaluator.Validate(c.Req.Context(), c.SignedInUser, evalCond); err != nil {
|
||||
return ErrResp(http.StatusBadRequest, err, "invalid condition")
|
||||
}
|
||||
|
||||
@@ -111,10 +110,6 @@ func (srv TestingApiSrv) RouteEvalQueries(c *models.ReqContext, cmd apimodels.Ev
|
||||
return ErrResp(http.StatusUnauthorized, fmt.Errorf("%w to query one or many data sources used by the rule", ErrAuthorization), "")
|
||||
}
|
||||
|
||||
if _, err := validateQueriesAndExpressions(c.Req.Context(), cmd.Data, c.SignedInUser, c.SkipCache, srv.DatasourceCache); err != nil {
|
||||
return ErrResp(http.StatusBadRequest, err, "invalid queries or expressions")
|
||||
}
|
||||
|
||||
evalResults, err := srv.evaluator.QueriesAndExpressionsEval(c.Req.Context(), c.SignedInUser, cmd.Data, now)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusBadRequest, err, "Failed to evaluate queries and expressions")
|
||||
|
||||
@@ -70,6 +70,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
|
||||
|
||||
evaluator := &eval.FakeEvaluator{}
|
||||
var result []eval.Result
|
||||
evaluator.EXPECT().Validate(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
evaluator.EXPECT().ConditionEval(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(result)
|
||||
|
||||
srv := createTestingApiSrv(ds, ac, evaluator)
|
||||
@@ -86,6 +87,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, response.Status())
|
||||
|
||||
evaluator.AssertCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||
evaluator.AssertCalled(t, "Validate", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -110,6 +112,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
|
||||
|
||||
evaluator := &eval.FakeEvaluator{}
|
||||
var result []eval.Result
|
||||
evaluator.EXPECT().Validate(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
evaluator.EXPECT().ConditionEval(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(result)
|
||||
|
||||
srv := createTestingApiSrv(ds, ac, evaluator)
|
||||
@@ -125,6 +128,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, response.Status())
|
||||
evaluator.AssertNotCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||
evaluator.AssertNotCalled(t, "Validate", mock.Anything, mock.Anything, mock.Anything)
|
||||
|
||||
rc.IsSignedIn = true
|
||||
|
||||
@@ -140,6 +144,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, response.Status())
|
||||
|
||||
evaluator.AssertCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||
evaluator.AssertCalled(t, "Validate", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -23,7 +22,6 @@ import (
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -207,63 +205,6 @@ func messageExtractor(resp *response.NormalResponse) (interface{}, error) {
|
||||
return map[string]string{"message": string(resp.Body())}, nil
|
||||
}
|
||||
|
||||
func validateCondition(ctx context.Context, c ngmodels.Condition, user *user.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) error {
|
||||
if len(c.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
refIDs, err := validateQueriesAndExpressions(ctx, c.Data, user, skipCache, datasourceCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := make([]string, 0, len(refIDs))
|
||||
for refID := range refIDs {
|
||||
t = append(t, refID)
|
||||
}
|
||||
if _, ok := refIDs[c.Condition]; !ok {
|
||||
return fmt.Errorf("condition %s not found in any query or expression: it should be one of: [%s]", c.Condition, strings.Join(t, ","))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// conditionValidator returns a curried validateCondition that accepts only condition
|
||||
func conditionValidator(c *models.ReqContext, cache datasources.CacheService) func(ngmodels.Condition) error {
|
||||
return func(condition ngmodels.Condition) error {
|
||||
return validateCondition(c.Req.Context(), condition, c.SignedInUser, c.SkipCache, cache)
|
||||
}
|
||||
}
|
||||
|
||||
func validateQueriesAndExpressions(ctx context.Context, data []ngmodels.AlertQuery, user *user.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) (map[string]struct{}, error) {
|
||||
refIDs := make(map[string]struct{})
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, query := range data {
|
||||
datasourceUID, err := query.GetDatasource()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isExpression, err := query.IsExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isExpression {
|
||||
refIDs[query.RefID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = datasourceCache.GetDatasourceByUID(ctx, datasourceUID, user, skipCache)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid query %s: %w: %s", query.RefID, err, datasourceUID)
|
||||
}
|
||||
refIDs[query.RefID] = struct{}{}
|
||||
}
|
||||
return refIDs, nil
|
||||
}
|
||||
|
||||
// ErrorResp creates a response with a visible error
|
||||
func ErrResp(status int, err error, msg string, args ...interface{}) *response.NormalResponse {
|
||||
if msg != "" {
|
||||
|
||||
Reference in New Issue
Block a user