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:
co-authored by
George Robinson
parent
94881597d8
commit
b963defa44
@@ -1,12 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/rand"
|
||||
@@ -98,6 +98,90 @@ func randFolder() *folder.Folder {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCondition(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
condition string
|
||||
data []apimodels.AlertQuery
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "error when condition is empty",
|
||||
condition: "",
|
||||
data: []apimodels.AlertQuery{},
|
||||
errorMsg: "condition cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "error when data is empty",
|
||||
condition: "A",
|
||||
data: []apimodels.AlertQuery{},
|
||||
errorMsg: "no query/expressions specified",
|
||||
},
|
||||
{
|
||||
name: "error when condition does not exist",
|
||||
condition: "A",
|
||||
data: []apimodels.AlertQuery{
|
||||
{
|
||||
RefID: "B",
|
||||
},
|
||||
{
|
||||
RefID: "C",
|
||||
},
|
||||
},
|
||||
errorMsg: "condition A does not exist, must be one of [B,C]",
|
||||
},
|
||||
{
|
||||
name: "error when duplicated refId",
|
||||
condition: "A",
|
||||
data: []apimodels.AlertQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
},
|
||||
{
|
||||
RefID: "A",
|
||||
},
|
||||
},
|
||||
errorMsg: "refID 'A' is already used by query/expression at index 0",
|
||||
},
|
||||
{
|
||||
name: "error when refId is empty",
|
||||
condition: "A",
|
||||
data: []apimodels.AlertQuery{
|
||||
{
|
||||
RefID: "",
|
||||
},
|
||||
{
|
||||
RefID: "A",
|
||||
},
|
||||
},
|
||||
errorMsg: "refID is not specified for data query/expression at index 0",
|
||||
},
|
||||
{
|
||||
name: "valid case",
|
||||
condition: "B",
|
||||
data: []apimodels.AlertQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
},
|
||||
{
|
||||
RefID: "B",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateCondition(tc.condition, tc.data)
|
||||
if tc.errorMsg == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.errorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleGroup(t *testing.T) {
|
||||
orgId := rand.Int63()
|
||||
folder := randFolder()
|
||||
@@ -110,22 +194,15 @@ func TestValidateRuleGroup(t *testing.T) {
|
||||
|
||||
t.Run("should validate struct and rules", func(t *testing.T) {
|
||||
g := validGroup(cfg, rules...)
|
||||
conditionValidations := 0
|
||||
alerts, err := validateRuleGroup(&g, orgId, folder, func(condition models.Condition) error {
|
||||
conditionValidations++
|
||||
return nil
|
||||
}, cfg)
|
||||
alerts, err := validateRuleGroup(&g, orgId, folder, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, alerts, len(rules))
|
||||
require.Equal(t, len(rules), conditionValidations)
|
||||
})
|
||||
|
||||
t.Run("should default to default interval from config if group interval is 0", func(t *testing.T) {
|
||||
g := validGroup(cfg, rules...)
|
||||
g.Interval = 0
|
||||
alerts, err := validateRuleGroup(&g, orgId, folder, func(condition models.Condition) error {
|
||||
return nil
|
||||
}, cfg)
|
||||
alerts, err := validateRuleGroup(&g, orgId, folder, cfg)
|
||||
require.NoError(t, err)
|
||||
for _, alert := range alerts {
|
||||
require.Equal(t, int64(cfg.DefaultRuleEvaluationInterval.Seconds()), alert.IntervalSeconds)
|
||||
@@ -140,9 +217,7 @@ func TestValidateRuleGroup(t *testing.T) {
|
||||
isPaused = !(isPaused)
|
||||
}
|
||||
g := validGroup(cfg, rules...)
|
||||
alerts, err := validateRuleGroup(&g, orgId, folder, func(condition models.Condition) error {
|
||||
return nil
|
||||
}, cfg)
|
||||
alerts, err := validateRuleGroup(&g, orgId, folder, cfg)
|
||||
require.NoError(t, err)
|
||||
for _, alert := range alerts {
|
||||
require.True(t, alert.HasPause)
|
||||
@@ -214,9 +289,7 @@ func TestValidateRuleGroupFailures(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
g := testCase.group()
|
||||
_, err := validateRuleGroup(g, orgId, folder, func(condition models.Condition) error {
|
||||
return nil
|
||||
}, cfg)
|
||||
_, err := validateRuleGroup(g, orgId, folder, cfg)
|
||||
require.Error(t, err)
|
||||
if testCase.assert != nil {
|
||||
testCase.assert(t, g, err)
|
||||
@@ -323,9 +396,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) {
|
||||
r := testCase.rule()
|
||||
r.GrafanaManagedAlert.UID = ""
|
||||
|
||||
alert, err := validateRuleNode(r, name, interval, orgId, folder, func(condition models.Condition) error {
|
||||
return nil
|
||||
}, cfg)
|
||||
alert, err := validateRuleNode(r, name, interval, orgId, folder, cfg)
|
||||
require.NoError(t, err)
|
||||
testCase.assert(t, r, alert)
|
||||
})
|
||||
@@ -333,9 +404,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) {
|
||||
|
||||
t.Run("accepts empty group name", func(t *testing.T) {
|
||||
r := validRule()
|
||||
alert, err := validateRuleNode(&r, "", interval, orgId, folder, func(condition models.Condition) error {
|
||||
return nil
|
||||
}, cfg)
|
||||
alert, err := validateRuleNode(&r, "", interval, orgId, folder, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", alert.RuleGroup)
|
||||
})
|
||||
@@ -345,17 +414,13 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
|
||||
orgId := rand.Int63()
|
||||
folder := randFolder()
|
||||
cfg := config(t)
|
||||
successValidation := func(condition models.Condition) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
interval *time.Duration
|
||||
rule func() *apimodels.PostableExtendedRuleNode
|
||||
conditionValidation func(condition models.Condition) error
|
||||
assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, err error)
|
||||
allowedIfNoUId bool
|
||||
name string
|
||||
interval *time.Duration
|
||||
rule func() *apimodels.PostableExtendedRuleNode
|
||||
assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, err error)
|
||||
allowedIfNoUId bool
|
||||
}{
|
||||
{
|
||||
name: "fail if GrafanaManagedAlert is not specified",
|
||||
@@ -415,16 +480,6 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
|
||||
return &r
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if validator function returns error",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
r := validRule()
|
||||
return &r
|
||||
},
|
||||
conditionValidation: func(condition models.Condition) error {
|
||||
return errors.New("BAD alert condition")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if Dashboard UID is specified but not Panel ID",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
@@ -456,6 +511,38 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
|
||||
return &r
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if Condition is empty",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
r := validRule()
|
||||
r.GrafanaManagedAlert.Condition = ""
|
||||
return &r
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if Data is empty",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
r := validRule()
|
||||
r.GrafanaManagedAlert.Data = nil
|
||||
return &r
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if Condition does not exist",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
r := validRule()
|
||||
r.GrafanaManagedAlert.Condition = uuid.NewString()
|
||||
return &r
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if Data has duplicate ref ID",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
r := validRule()
|
||||
r.GrafanaManagedAlert.Data = append(r.GrafanaManagedAlert.Data, r.GrafanaManagedAlert.Data...)
|
||||
return &r
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -464,17 +551,13 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
|
||||
if r.GrafanaManagedAlert != nil {
|
||||
r.GrafanaManagedAlert.UID = ""
|
||||
}
|
||||
f := successValidation
|
||||
if testCase.conditionValidation != nil {
|
||||
f = testCase.conditionValidation
|
||||
}
|
||||
|
||||
interval := cfg.BaseInterval
|
||||
if testCase.interval != nil {
|
||||
interval = *testCase.interval
|
||||
}
|
||||
|
||||
_, err := validateRuleNode(r, "", interval, orgId, folder, f, cfg)
|
||||
_, err := validateRuleNode(r, "", interval, orgId, folder, cfg)
|
||||
require.Error(t, err)
|
||||
if testCase.assert != nil {
|
||||
testCase.assert(t, r, err)
|
||||
@@ -566,9 +649,7 @@ func TestValidateRuleNode_UID(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
r := testCase.rule()
|
||||
alert, err := validateRuleNode(r, name, interval, orgId, folder, func(condition models.Condition) error {
|
||||
return nil
|
||||
}, cfg)
|
||||
alert, err := validateRuleNode(r, name, interval, orgId, folder, cfg)
|
||||
require.NoError(t, err)
|
||||
testCase.assert(t, r, alert)
|
||||
})
|
||||
@@ -576,9 +657,7 @@ func TestValidateRuleNode_UID(t *testing.T) {
|
||||
|
||||
t.Run("accepts empty group name", func(t *testing.T) {
|
||||
r := validRule()
|
||||
alert, err := validateRuleNode(&r, "", interval, orgId, folder, func(condition models.Condition) error {
|
||||
return nil
|
||||
}, cfg)
|
||||
alert, err := validateRuleNode(&r, "", interval, orgId, folder, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", alert.RuleGroup)
|
||||
})
|
||||
@@ -588,16 +667,12 @@ func TestValidateRuleNodeFailures_UID(t *testing.T) {
|
||||
orgId := rand.Int63()
|
||||
folder := randFolder()
|
||||
cfg := config(t)
|
||||
successValidation := func(condition models.Condition) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
interval *time.Duration
|
||||
rule func() *apimodels.PostableExtendedRuleNode
|
||||
conditionValidation func(condition models.Condition) error
|
||||
assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, err error)
|
||||
name string
|
||||
interval *time.Duration
|
||||
rule func() *apimodels.PostableExtendedRuleNode
|
||||
assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, err error)
|
||||
}{
|
||||
{
|
||||
name: "fail if GrafanaManagedAlert is not specified",
|
||||
@@ -635,16 +710,6 @@ func TestValidateRuleNodeFailures_UID(t *testing.T) {
|
||||
return &r
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if validator function returns error",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
r := validRule()
|
||||
return &r
|
||||
},
|
||||
conditionValidation: func(condition models.Condition) error {
|
||||
return errors.New("BAD alert condition")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail if Dashboard UID is specified but not Panel ID",
|
||||
rule: func() *apimodels.PostableExtendedRuleNode {
|
||||
@@ -681,17 +746,13 @@ func TestValidateRuleNodeFailures_UID(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
r := testCase.rule()
|
||||
f := successValidation
|
||||
if testCase.conditionValidation != nil {
|
||||
f = testCase.conditionValidation
|
||||
}
|
||||
|
||||
interval := cfg.BaseInterval
|
||||
if testCase.interval != nil {
|
||||
interval = *testCase.interval
|
||||
}
|
||||
|
||||
_, err := validateRuleNode(r, "", interval, orgId, folder, f, cfg)
|
||||
_, err := validateRuleNode(r, "", interval, orgId, folder, cfg)
|
||||
require.Error(t, err)
|
||||
if testCase.assert != nil {
|
||||
testCase.assert(t, r, err)
|
||||
@@ -724,11 +785,7 @@ func TestValidateRuleNodeIntervalFailures(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
r := validRule()
|
||||
f := func(condition models.Condition) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := validateRuleNode(&r, util.GenerateShortUID(), testCase.interval, rand.Int63(), randFolder(), f, cfg)
|
||||
_, err := validateRuleNode(&r, util.GenerateShortUID(), testCase.interval, rand.Int63(), randFolder(), cfg)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user