mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: validate condition before updating rulegroup (#33367)
* Alerting: validate condition before updating rulegroup * Apply suggestions from code review
This commit is contained in:
parent
643e7af3e0
commit
7ccb022c03
@ -84,7 +84,7 @@ func (api *API) RegisterAPIEndpoints() {
|
|||||||
api.RegisterRulerApiEndpoints(NewForkedRuler(
|
api.RegisterRulerApiEndpoints(NewForkedRuler(
|
||||||
api.DatasourceCache,
|
api.DatasourceCache,
|
||||||
NewLotexRuler(proxy, logger),
|
NewLotexRuler(proxy, logger),
|
||||||
RulerSrv{store: api.RuleStore, log: logger},
|
RulerSrv{DatasourceCache: api.DatasourceCache, store: api.RuleStore, log: logger},
|
||||||
reg,
|
reg,
|
||||||
))
|
))
|
||||||
api.RegisterTestingApiEndpoints(TestingApiSrv{
|
api.RegisterTestingApiEndpoints(TestingApiSrv{
|
||||||
|
@ -2,9 +2,11 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
|
|
||||||
coreapi "github.com/grafana/grafana/pkg/api"
|
coreapi "github.com/grafana/grafana/pkg/api"
|
||||||
@ -18,8 +20,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RulerSrv struct {
|
type RulerSrv struct {
|
||||||
store store.RuleStore
|
store store.RuleStore
|
||||||
log log.Logger
|
DatasourceCache datasources.CacheService
|
||||||
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response {
|
func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response {
|
||||||
@ -196,6 +199,17 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
|
|||||||
return response.Error(http.StatusBadRequest, "rule group name is not valid", nil)
|
return response.Error(http.StatusBadRequest, "rule group name is not valid", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, r := range ruleGroupConfig.Rules {
|
||||||
|
cond := ngmodels.Condition{
|
||||||
|
Condition: r.GrafanaManagedAlert.Condition,
|
||||||
|
OrgID: c.SignedInUser.OrgId,
|
||||||
|
Data: r.GrafanaManagedAlert.Data,
|
||||||
|
}
|
||||||
|
if err := validateCondition(cond, c.SignedInUser, c.SkipCache, srv.DatasourceCache); err != nil {
|
||||||
|
return response.Error(http.StatusBadRequest, fmt.Sprintf("failed to validate alert rule %s", r.GrafanaManagedAlert.Title), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{
|
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{
|
||||||
OrgID: c.SignedInUser.OrgId,
|
OrgID: c.SignedInUser.OrgId,
|
||||||
NamespaceUID: namespace.Uid,
|
NamespaceUID: namespace.Uid,
|
||||||
|
@ -84,7 +84,8 @@ func (srv TestingApiSrv) RouteEvalQueries(c *models.ReqContext, cmd apimodels.Ev
|
|||||||
if now.IsZero() {
|
if now.IsZero() {
|
||||||
now = timeNow()
|
now = timeNow()
|
||||||
}
|
}
|
||||||
if err := validateQueriesAndExpressions(cmd.Data, c.SignedInUser, c.SkipCache, srv.DatasourceCache); err != nil {
|
|
||||||
|
if _, err := validateQueriesAndExpressions(cmd.Data, c.SignedInUser, c.SkipCache, srv.DatasourceCache); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "invalid queries or expressions", err)
|
return response.Error(http.StatusBadRequest, "invalid queries or expressions", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,67 +158,53 @@ func messageExtractor(b []byte) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateCondition(c ngmodels.Condition, user *models.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) error {
|
func validateCondition(c ngmodels.Condition, user *models.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) error {
|
||||||
var refID string
|
|
||||||
|
|
||||||
if len(c.Data) == 0 {
|
if len(c.Data) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range c.Data {
|
refIDs, err := validateQueriesAndExpressions(c.Data, user, skipCache, datasourceCache)
|
||||||
if c.Condition == query.RefID {
|
if err != nil {
|
||||||
refID = c.Condition
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
datasourceUID, err := query.GetDatasource()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isExpression, err := query.IsExpression()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isExpression {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = datasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if refID == "" {
|
t := make([]string, 0, len(refIDs))
|
||||||
return fmt.Errorf("condition %s not found in any query or expression", c.Condition)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateQueriesAndExpressions(data []ngmodels.AlertQuery, user *models.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) error {
|
func validateQueriesAndExpressions(data []ngmodels.AlertQuery, user *models.SignedInUser, skipCache bool, datasourceCache datasources.CacheService) (map[string]struct{}, error) {
|
||||||
|
refIDs := make(map[string]struct{})
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range data {
|
for _, query := range data {
|
||||||
datasourceUID, err := query.GetDatasource()
|
datasourceUID, err := query.GetDatasource()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
isExpression, err := query.IsExpression()
|
isExpression, err := query.IsExpression()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if isExpression {
|
if isExpression {
|
||||||
|
refIDs[query.RefID] = struct{}{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = datasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
|
_, err = datasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
|
return nil, fmt.Errorf("invalid query %s: %w: %s", query.RefID, err, datasourceUID)
|
||||||
}
|
}
|
||||||
|
refIDs[query.RefID] = struct{}{}
|
||||||
}
|
}
|
||||||
return nil
|
return refIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func conditionEval(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand, datasourceCache datasources.CacheService, dataService *tsdb.Service, cfg *setting.Cfg) response.Response {
|
func conditionEval(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand, datasourceCache datasources.CacheService, dataService *tsdb.Service, cfg *setting.Cfg) response.Response {
|
||||||
|
@ -183,8 +183,8 @@ func TestAlertRuleCRUD(t *testing.T) {
|
|||||||
Annotations: map[string]string{"annotation1": "val1"},
|
Annotations: map[string]string{"annotation1": "val1"},
|
||||||
},
|
},
|
||||||
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||||
Title: "AlwaysFiring",
|
Title: "AlwaysFiring",
|
||||||
Condition: "A",
|
Data: []ngmodels.AlertQuery{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedResponse: `{"error":"invalid alert rule: no queries or expressions are found", "message":"failed to update rule group"}`,
|
expectedResponse: `{"error":"invalid alert rule: no queries or expressions are found", "message":"failed to update rule group"}`,
|
||||||
@ -310,6 +310,66 @@ func TestAlertRuleCRUD(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedResponse: `{"error":"invalid alert rule: interval (1s) should be divided exactly by scheduler interval: 10s", "message":"failed to update rule group"}`,
|
expectedResponse: `{"error":"invalid alert rule: interval (1s) should be divided exactly by scheduler interval: 10s", "message":"failed to update rule group"}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "alert rule with unknown datasource",
|
||||||
|
rulegroup: "arulegroup",
|
||||||
|
rule: apimodels.PostableExtendedRuleNode{
|
||||||
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
||||||
|
For: interval,
|
||||||
|
Labels: map[string]string{"label1": "val1"},
|
||||||
|
Annotations: map[string]string{"annotation1": "val1"},
|
||||||
|
},
|
||||||
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||||
|
Title: "AlwaysFiring",
|
||||||
|
Condition: "A",
|
||||||
|
Data: []ngmodels.AlertQuery{
|
||||||
|
{
|
||||||
|
RefID: "A",
|
||||||
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
||||||
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
||||||
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
||||||
|
},
|
||||||
|
DatasourceUID: "unknown",
|
||||||
|
Model: json.RawMessage(`{
|
||||||
|
"type": "math",
|
||||||
|
"expression": "2 + 3 > 1"
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedResponse: `{"error":"invalid query A: data source not found: unknown", "message":"failed to validate alert rule AlwaysFiring"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "alert rule with invalid condition",
|
||||||
|
rulegroup: "arulegroup",
|
||||||
|
rule: apimodels.PostableExtendedRuleNode{
|
||||||
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
||||||
|
For: interval,
|
||||||
|
Labels: map[string]string{"label1": "val1"},
|
||||||
|
Annotations: map[string]string{"annotation1": "val1"},
|
||||||
|
},
|
||||||
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||||
|
Title: "AlwaysFiring",
|
||||||
|
Condition: "B",
|
||||||
|
Data: []ngmodels.AlertQuery{
|
||||||
|
{
|
||||||
|
RefID: "A",
|
||||||
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
||||||
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
||||||
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
||||||
|
},
|
||||||
|
DatasourceUID: "-100",
|
||||||
|
Model: json.RawMessage(`{
|
||||||
|
"type": "math",
|
||||||
|
"expression": "2 + 3 > 1"
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedResponse: `{"error":"condition B not found in any query or expression: it should be one of: [A]", "message":"failed to validate alert rule AlwaysFiring"}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@ -931,7 +991,7 @@ func TestAlertRuleCRUD(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectedStatusCode: http.StatusBadRequest,
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
expectedResponse: `{"error":"condition B not found in any query or expression","message":"invalid condition"}`,
|
expectedResponse: `{"error":"condition B not found in any query or expression: it should be one of: [A]","message":"invalid condition"}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "unknown query datasource",
|
desc: "unknown query datasource",
|
||||||
@ -956,7 +1016,7 @@ func TestAlertRuleCRUD(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectedStatusCode: http.StatusBadRequest,
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
expectedResponse: `{"error":"failed to get datasource: unknown: data source not found","message":"invalid condition"}`,
|
expectedResponse: `{"error":"invalid query A: data source not found: unknown","message":"invalid condition"}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1112,7 +1172,7 @@ func TestAlertRuleCRUD(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectedStatusCode: http.StatusBadRequest,
|
expectedStatusCode: http.StatusBadRequest,
|
||||||
expectedResponse: `{"error":"failed to get datasource: unknown: data source not found","message":"invalid queries or expressions"}`,
|
expectedResponse: `{"error":"invalid query A: data source not found: unknown","message":"invalid queries or expressions"}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user