Alerting: update rules POST API to validate query and condition only for rules that changed. (#68667)

* replace condition validation with just structural validation
* validate conditions of only new and updated rules
* add integration tests for rule update\delete API

Co-authored-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
Yuri Tseretyan
2023-06-15 13:33:42 -04:00
committed by GitHub
co-authored by George Robinson
parent 94881597d8
commit b963defa44
10 changed files with 542 additions and 108 deletions
@@ -3,6 +3,7 @@ package api
import (
"errors"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/services/folder"
@@ -19,7 +20,6 @@ func validateRuleNode(
interval time.Duration,
orgId int64,
namespace *folder.Folder,
conditionValidator func(ngmodels.Condition) error,
cfg *setting.UnifiedAlertingSettings) (*ngmodels.AlertRule, error) {
intervalSeconds, err := validateInterval(cfg, interval)
if err != nil {
@@ -74,18 +74,14 @@ func validateRuleNode(
} else {
return nil, fmt.Errorf("%w: no queries or expressions are found", ngmodels.ErrAlertRuleFailedValidation)
}
} else {
err = validateCondition(ruleNode.GrafanaManagedAlert.Condition, ruleNode.GrafanaManagedAlert.Data)
if err != nil {
return nil, fmt.Errorf("%w: %s", ngmodels.ErrAlertRuleFailedValidation, err.Error())
}
}
queries := AlertQueriesFromApiAlertQueries(ruleNode.GrafanaManagedAlert.Data)
if len(queries) != 0 {
cond := ngmodels.Condition{
Condition: ruleNode.GrafanaManagedAlert.Condition,
Data: queries,
}
if err = conditionValidator(cond); err != nil {
return nil, fmt.Errorf("failed to validate condition of alert rule %s: %w", ruleNode.GrafanaManagedAlert.Title, err)
}
}
newAlertRule := ngmodels.AlertRule{
OrgID: orgId,
@@ -117,6 +113,33 @@ func validateRuleNode(
return &newAlertRule, nil
}
func validateCondition(condition string, queries []apimodels.AlertQuery) error {
if condition == "" {
return errors.New("condition cannot be empty")
}
if len(queries) == 0 {
return errors.New("no query/expressions specified")
}
refIDs := make(map[string]int, len(queries))
for idx, query := range queries {
if query.RefID == "" {
return fmt.Errorf("refID is not specified for data query/expression at index %d", idx)
}
if usedIdx, ok := refIDs[query.RefID]; ok {
return fmt.Errorf("refID '%s' is already used by query/expression at index %d", query.RefID, usedIdx)
}
refIDs[query.RefID] = idx
}
if _, ok := refIDs[condition]; !ok {
ids := make([]string, 0, len(refIDs))
for id := range refIDs {
ids = append(ids, id)
}
return fmt.Errorf("condition %s does not exist, must be one of [%s]", condition, strings.Join(ids, ","))
}
return nil
}
func validateInterval(cfg *setting.UnifiedAlertingSettings, interval time.Duration) (int64, error) {
intervalSeconds := int64(interval.Seconds())
@@ -155,7 +178,6 @@ func validateRuleGroup(
ruleGroupConfig *apimodels.PostableRuleGroupConfig,
orgId int64,
namespace *folder.Folder,
conditionValidator func(ngmodels.Condition) error,
cfg *setting.UnifiedAlertingSettings) ([]*ngmodels.AlertRuleWithOptionals, error) {
if ruleGroupConfig.Name == "" {
return nil, errors.New("rule group name cannot be empty")
@@ -180,7 +202,7 @@ func validateRuleGroup(
result := make([]*ngmodels.AlertRuleWithOptionals, 0, len(ruleGroupConfig.Rules))
uids := make(map[string]int, cap(result))
for idx := range ruleGroupConfig.Rules {
rule, err := validateRuleNode(&ruleGroupConfig.Rules[idx], ruleGroupConfig.Name, interval, orgId, namespace, conditionValidator, cfg)
rule, err := validateRuleNode(&ruleGroupConfig.Rules[idx], ruleGroupConfig.Name, interval, orgId, namespace, cfg)
// TODO do not stop on the first failure but return all failures
if err != nil {
return nil, fmt.Errorf("invalid rule specification at index [%d]: %w", idx, err)