mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
49c8deb1ea
* Read path, main API * Define record field for incoming requests * Refactor several alerting specific validators into two paths * Refactor validateCondition actually contain all the condition validation logic * Move condition validation inside rule path * Validators for recording rules * Wire feature flag through to validators * Test for accepting a valid recording rule * Tests for negative case, no UID * Test for ignoring alerting fields * Build conditions based on recording rules as well * Regenerate swagger docs * Fix CRUD test to cover the right thing * Re-generate swagger docs with backdated v0.30.2 version * Regenerate base spec * Regenerate ngalert specs * Regenerate top level specs * Comment and rename * Return struct instead of modifying ref
1065 lines
32 KiB
Go
1065 lines
32 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/rand"
|
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
var allNoData = []apimodels.NoDataState{
|
|
apimodels.OK,
|
|
apimodels.NoData,
|
|
apimodels.Alerting,
|
|
}
|
|
|
|
var allExecError = []apimodels.ExecutionErrorState{
|
|
apimodels.ErrorErrState,
|
|
apimodels.AlertingErrState,
|
|
}
|
|
|
|
func config(t *testing.T) *setting.UnifiedAlertingSettings {
|
|
t.Helper()
|
|
baseInterval := time.Duration(rand.Intn(99)+1) * time.Second
|
|
result := &setting.UnifiedAlertingSettings{
|
|
BaseInterval: baseInterval,
|
|
DefaultRuleEvaluationInterval: baseInterval * time.Duration(rand.Intn(9)+1),
|
|
}
|
|
t.Logf("Config Base interval is [%v]", result.BaseInterval)
|
|
return result
|
|
}
|
|
|
|
func makeLimits(cfg *setting.UnifiedAlertingSettings) RuleLimits {
|
|
baseToggles := featuremgmt.WithFeatures()
|
|
return RuleLimitsFromConfig(cfg, baseToggles)
|
|
}
|
|
|
|
func allowRecording(lim RuleLimits) *RuleLimits {
|
|
lim.RecordingRulesAllowed = true
|
|
return &lim
|
|
}
|
|
|
|
func validRule() apimodels.PostableExtendedRuleNode {
|
|
forDuration := model.Duration(rand.Int63n(1000))
|
|
uid := util.GenerateShortUID()
|
|
return apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: &forDuration,
|
|
Labels: map[string]string{
|
|
"test-label": "data",
|
|
},
|
|
Annotations: map[string]string{
|
|
"test-annotation": "data",
|
|
},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: fmt.Sprintf("TEST-ALERT-%s", uid),
|
|
Condition: "A",
|
|
Data: []apimodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
QueryType: "TEST",
|
|
RelativeTimeRange: apimodels.RelativeTimeRange{
|
|
From: 10,
|
|
To: 0,
|
|
},
|
|
DatasourceUID: "DATASOURCE_TEST",
|
|
Model: nil,
|
|
},
|
|
},
|
|
UID: uid,
|
|
NoDataState: allNoData[rand.Intn(len(allNoData))],
|
|
ExecErrState: allExecError[rand.Intn(len(allExecError))],
|
|
},
|
|
}
|
|
}
|
|
|
|
func validGroup(cfg *setting.UnifiedAlertingSettings, rules ...apimodels.PostableExtendedRuleNode) apimodels.PostableRuleGroupConfig {
|
|
return apimodels.PostableRuleGroupConfig{
|
|
Name: "TEST-ALERTS-" + util.GenerateShortUID(),
|
|
Interval: model.Duration(cfg.BaseInterval * time.Duration(rand.Int63n(10))),
|
|
Rules: rules,
|
|
}
|
|
}
|
|
|
|
func randFolder() *folder.Folder {
|
|
title := "TEST-FOLDER-" + util.GenerateShortUID()
|
|
return &folder.Folder{
|
|
UID: util.GenerateShortUID(),
|
|
Title: title,
|
|
// URL: "",
|
|
// Version: 0,
|
|
Created: time.Time{},
|
|
Updated: time.Time{},
|
|
// UpdatedBy: 0,
|
|
// CreatedBy: 0,
|
|
// HasACL: false,
|
|
ParentUID: uuid.NewString(),
|
|
Fullpath: path.Join("parent-folder", title),
|
|
}
|
|
}
|
|
|
|
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 queries or expressions are found",
|
|
},
|
|
{
|
|
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, false)
|
|
if tc.errorMsg == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, tc.errorMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRuleGroup(t *testing.T) {
|
|
orgId := rand.Int63()
|
|
folder := randFolder()
|
|
|
|
rules := make([]apimodels.PostableExtendedRuleNode, 0, rand.Intn(4)+1)
|
|
for i := 0; i < cap(rules); i++ {
|
|
rules = append(rules, validRule())
|
|
}
|
|
cfg := config(t)
|
|
limits := makeLimits(cfg)
|
|
|
|
t.Run("should validate struct and rules", func(t *testing.T) {
|
|
g := validGroup(cfg, rules...)
|
|
alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, limits)
|
|
require.NoError(t, err)
|
|
require.Len(t, alerts, len(rules))
|
|
})
|
|
|
|
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.UID, limits)
|
|
require.NoError(t, err)
|
|
for _, alert := range alerts {
|
|
require.Equal(t, int64(cfg.DefaultRuleEvaluationInterval.Seconds()), alert.IntervalSeconds)
|
|
require.False(t, alert.HasPause)
|
|
}
|
|
})
|
|
|
|
t.Run("should show the payload has isPaused field", func(t *testing.T) {
|
|
for _, rule := range rules {
|
|
isPaused := true
|
|
rule.GrafanaManagedAlert.IsPaused = &isPaused
|
|
isPaused = !(isPaused)
|
|
}
|
|
g := validGroup(cfg, rules...)
|
|
alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, limits)
|
|
require.NoError(t, err)
|
|
for _, alert := range alerts {
|
|
require.True(t, alert.HasPause)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateRuleGroupFailures(t *testing.T) {
|
|
orgId := rand.Int63()
|
|
folder := randFolder()
|
|
cfg := config(t)
|
|
limits := makeLimits(cfg)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
group func() *apimodels.PostableRuleGroupConfig
|
|
assert func(t *testing.T, apiModel *apimodels.PostableRuleGroupConfig, err error)
|
|
}{
|
|
{
|
|
name: "fail if title is empty",
|
|
group: func() *apimodels.PostableRuleGroupConfig {
|
|
g := validGroup(cfg)
|
|
g.Name = ""
|
|
return &g
|
|
},
|
|
},
|
|
{
|
|
name: "fail if title is too long",
|
|
group: func() *apimodels.PostableRuleGroupConfig {
|
|
g := validGroup(cfg)
|
|
for len(g.Name) < store.AlertRuleMaxRuleGroupNameLength {
|
|
g.Name += g.Name
|
|
}
|
|
return &g
|
|
},
|
|
},
|
|
{
|
|
name: "fail if interval is negative",
|
|
group: func() *apimodels.PostableRuleGroupConfig {
|
|
g := validGroup(cfg)
|
|
g.Interval = model.Duration(-(rand.Int63n(1000) + 1))
|
|
return &g
|
|
},
|
|
},
|
|
{
|
|
name: "fail if interval is not aligned with base interval",
|
|
group: func() *apimodels.PostableRuleGroupConfig {
|
|
g := validGroup(cfg)
|
|
g.Interval = model.Duration(cfg.BaseInterval + time.Duration(rand.Intn(10)+1)*time.Second)
|
|
return &g
|
|
},
|
|
},
|
|
{
|
|
name: "fail if two rules have same UID",
|
|
group: func() *apimodels.PostableRuleGroupConfig {
|
|
r1 := validRule()
|
|
r2 := validRule()
|
|
uid := util.GenerateShortUID()
|
|
r1.GrafanaManagedAlert.UID = uid
|
|
r2.GrafanaManagedAlert.UID = uid
|
|
g := validGroup(cfg, r1, r2)
|
|
return &g
|
|
},
|
|
assert: func(t *testing.T, apiModel *apimodels.PostableRuleGroupConfig, err error) {
|
|
require.Contains(t, err.Error(), apiModel.Rules[0].GrafanaManagedAlert.UID)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
g := testCase.group()
|
|
_, err := ValidateRuleGroup(g, orgId, folder.UID, limits)
|
|
require.Error(t, err)
|
|
if testCase.assert != nil {
|
|
testCase.assert(t, g, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRuleNode_NoUID(t *testing.T) {
|
|
orgId := rand.Int63()
|
|
folder := randFolder()
|
|
name := util.GenerateShortUID()
|
|
var cfg = config(t)
|
|
limits := makeLimits(cfg)
|
|
interval := cfg.BaseInterval * time.Duration(rand.Int63n(10)+1)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
rule func() *apimodels.PostableExtendedRuleNode
|
|
limits *RuleLimits
|
|
assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, rule *models.AlertRule)
|
|
}{
|
|
{
|
|
name: "coverts api model to AlertRule",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, int64(0), alert.ID)
|
|
require.Equal(t, orgId, alert.OrgID)
|
|
require.Equal(t, api.GrafanaManagedAlert.Title, alert.Title)
|
|
require.Equal(t, api.GrafanaManagedAlert.Condition, alert.Condition)
|
|
require.Equal(t, AlertQueriesFromApiAlertQueries(api.GrafanaManagedAlert.Data), alert.Data)
|
|
require.Equal(t, time.Time{}, alert.Updated)
|
|
require.Equal(t, int64(interval.Seconds()), alert.IntervalSeconds)
|
|
require.Equal(t, int64(0), alert.Version)
|
|
require.Equal(t, api.GrafanaManagedAlert.UID, alert.UID)
|
|
require.Equal(t, folder.UID, alert.NamespaceUID)
|
|
require.Nil(t, alert.DashboardUID)
|
|
require.Nil(t, alert.PanelID)
|
|
require.Equal(t, name, alert.RuleGroup)
|
|
require.Equal(t, models.NoDataState(api.GrafanaManagedAlert.NoDataState), alert.NoDataState)
|
|
require.Equal(t, models.ExecutionErrorState(api.GrafanaManagedAlert.ExecErrState), alert.ExecErrState)
|
|
require.Equal(t, time.Duration(*api.ApiRuleNode.For), alert.For)
|
|
require.Equal(t, api.ApiRuleNode.Annotations, alert.Annotations)
|
|
require.Equal(t, api.ApiRuleNode.Labels, alert.Labels)
|
|
require.Nil(t, alert.Record)
|
|
},
|
|
},
|
|
{
|
|
name: "coverts api without ApiRuleNode",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode = nil
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, time.Duration(0), alert.For)
|
|
require.Nil(t, alert.Annotations)
|
|
require.Nil(t, alert.Labels)
|
|
},
|
|
},
|
|
{
|
|
name: "defaults to NoData if NoDataState is empty",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.NoDataState = ""
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, models.NoData, alert.NoDataState)
|
|
},
|
|
},
|
|
{
|
|
name: "defaults to Alerting if ExecErrState is empty",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.ExecErrState = ""
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, models.AlertingErrState, alert.ExecErrState)
|
|
},
|
|
},
|
|
{
|
|
name: "extracts Dashboard UID and Panel Id from annotations",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: util.GenerateShortUID(),
|
|
models.PanelIDAnnotation: strconv.Itoa(rand.Int()),
|
|
}
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, api.ApiRuleNode.Annotations[models.DashboardUIDAnnotation], *alert.DashboardUID)
|
|
panelId, err := strconv.Atoi(api.ApiRuleNode.Annotations[models.PanelIDAnnotation])
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(panelId), *alert.PanelID)
|
|
},
|
|
},
|
|
{
|
|
name: "accepts and converts recording rule when toggle is enabled",
|
|
limits: allowRecording(limits),
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"}
|
|
r.GrafanaManagedAlert.Condition = ""
|
|
r.GrafanaManagedAlert.NoDataState = ""
|
|
r.GrafanaManagedAlert.ExecErrState = ""
|
|
r.GrafanaManagedAlert.NotificationSettings = nil
|
|
r.ApiRuleNode.For = nil
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
// Shared fields
|
|
require.Equal(t, int64(0), alert.ID)
|
|
require.Equal(t, orgId, alert.OrgID)
|
|
require.Equal(t, api.GrafanaManagedAlert.Title, alert.Title)
|
|
require.Equal(t, AlertQueriesFromApiAlertQueries(api.GrafanaManagedAlert.Data), alert.Data)
|
|
require.Equal(t, time.Time{}, alert.Updated)
|
|
require.Equal(t, int64(interval.Seconds()), alert.IntervalSeconds)
|
|
require.Equal(t, int64(0), alert.Version)
|
|
require.Equal(t, api.GrafanaManagedAlert.UID, alert.UID)
|
|
require.Equal(t, folder.UID, alert.NamespaceUID)
|
|
require.Nil(t, alert.DashboardUID)
|
|
require.Nil(t, alert.PanelID)
|
|
require.Equal(t, name, alert.RuleGroup)
|
|
require.Equal(t, api.ApiRuleNode.Annotations, alert.Annotations)
|
|
require.Equal(t, api.ApiRuleNode.Labels, alert.Labels)
|
|
// Alerting fields
|
|
require.Empty(t, alert.Condition)
|
|
require.Empty(t, alert.NoDataState)
|
|
require.Empty(t, alert.ExecErrState)
|
|
require.Nil(t, alert.NotificationSettings)
|
|
require.Zero(t, alert.For)
|
|
// Recording fields
|
|
require.Equal(t, api.GrafanaManagedAlert.Record.From, alert.Record.From)
|
|
require.Equal(t, api.GrafanaManagedAlert.Record.Metric, alert.Record.Metric)
|
|
},
|
|
},
|
|
{
|
|
name: "recording rules ignore fields that only make sense for Alerting rules",
|
|
limits: allowRecording(limits),
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"}
|
|
r.GrafanaManagedAlert.Condition = "A"
|
|
r.GrafanaManagedAlert.NoDataState = apimodels.OK
|
|
r.GrafanaManagedAlert.ExecErrState = apimodels.AlertingErrState
|
|
r.GrafanaManagedAlert.NotificationSettings = &apimodels.AlertRuleNotificationSettings{}
|
|
r.ApiRuleNode.For = func() *model.Duration { five := model.Duration(time.Second * 5); return &five }()
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Empty(t, alert.Condition)
|
|
require.Empty(t, alert.NoDataState)
|
|
require.Empty(t, alert.ExecErrState)
|
|
require.Nil(t, alert.NotificationSettings)
|
|
require.Zero(t, alert.For)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
r := testCase.rule()
|
|
r.GrafanaManagedAlert.UID = ""
|
|
lim := limits
|
|
if testCase.limits != nil {
|
|
lim = *testCase.limits
|
|
}
|
|
|
|
alert, err := validateRuleNode(r, name, interval, orgId, folder.UID, lim)
|
|
require.NoError(t, err)
|
|
testCase.assert(t, r, alert)
|
|
})
|
|
}
|
|
|
|
t.Run("accepts empty group name", func(t *testing.T) {
|
|
r := validRule()
|
|
alert, err := validateRuleNode(&r, "", interval, orgId, folder.UID, limits)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", alert.RuleGroup)
|
|
})
|
|
}
|
|
|
|
func TestValidateRuleNodeFailures_NoUID(t *testing.T) {
|
|
orgId := rand.Int63()
|
|
folder := randFolder()
|
|
cfg := config(t)
|
|
limits := makeLimits(cfg)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
interval *time.Duration
|
|
rule func() *apimodels.PostableExtendedRuleNode
|
|
limits *RuleLimits
|
|
expErr string
|
|
assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, err error)
|
|
allowedIfNoUId bool
|
|
}{
|
|
{
|
|
name: "fail if GrafanaManagedAlert is not specified",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert = nil
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if title is empty",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Title = ""
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if title is too long",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
for len(r.GrafanaManagedAlert.Title) < store.AlertRuleMaxTitleLength {
|
|
r.GrafanaManagedAlert.Title += r.GrafanaManagedAlert.Title
|
|
}
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if NoDataState is not known",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.NoDataState = apimodels.NoDataState(util.GenerateShortUID())
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if ExecErrState is not known",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.ExecErrState = apimodels.ExecutionErrorState(util.GenerateShortUID())
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if there are not data (nil)",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Data = nil
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if there are not data (empty)",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Data = make([]apimodels.AlertQuery, 0, 1)
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if Dashboard UID is specified but not Panel ID",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: util.GenerateShortUID(),
|
|
}
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if Dashboard UID is specified and Panel ID is NaN",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: util.GenerateShortUID(),
|
|
models.PanelIDAnnotation: util.GenerateShortUID(),
|
|
}
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if PanelID is specified but not Dashboard UID ",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.PanelIDAnnotation: "0",
|
|
}
|
|
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
|
|
},
|
|
},
|
|
{
|
|
name: "rejects valid recording rules if toggle is disabled",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "some_metric", From: "A"}
|
|
r.GrafanaManagedAlert.Condition = ""
|
|
r.GrafanaManagedAlert.NoDataState = ""
|
|
r.GrafanaManagedAlert.ExecErrState = ""
|
|
r.GrafanaManagedAlert.NotificationSettings = nil
|
|
r.ApiRuleNode.For = nil
|
|
return &r
|
|
},
|
|
expErr: "recording rules cannot be created",
|
|
},
|
|
{
|
|
name: "rejects recording rule with invalid metric name",
|
|
limits: allowRecording(limits),
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "", From: "A"}
|
|
r.GrafanaManagedAlert.Condition = ""
|
|
r.GrafanaManagedAlert.NoDataState = ""
|
|
r.GrafanaManagedAlert.ExecErrState = ""
|
|
r.GrafanaManagedAlert.NotificationSettings = nil
|
|
r.ApiRuleNode.For = nil
|
|
return &r
|
|
},
|
|
expErr: "must be a valid Prometheus metric name",
|
|
},
|
|
{
|
|
name: "rejects recording rule with empty from",
|
|
limits: allowRecording(limits),
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "my_metric", From: ""}
|
|
r.GrafanaManagedAlert.Condition = ""
|
|
r.GrafanaManagedAlert.NoDataState = ""
|
|
r.GrafanaManagedAlert.ExecErrState = ""
|
|
r.GrafanaManagedAlert.NotificationSettings = nil
|
|
r.ApiRuleNode.For = nil
|
|
return &r
|
|
},
|
|
expErr: "cannot be empty",
|
|
},
|
|
{
|
|
name: "rejects recording rule with from not matching",
|
|
limits: allowRecording(limits),
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Record = &apimodels.Record{Metric: "my_metric", From: "NOTEXIST"}
|
|
r.GrafanaManagedAlert.Condition = ""
|
|
r.GrafanaManagedAlert.NoDataState = ""
|
|
r.GrafanaManagedAlert.ExecErrState = ""
|
|
r.GrafanaManagedAlert.NotificationSettings = nil
|
|
r.ApiRuleNode.For = nil
|
|
return &r
|
|
},
|
|
expErr: "NOTEXIST does not exist",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
r := testCase.rule()
|
|
if r.GrafanaManagedAlert != nil {
|
|
r.GrafanaManagedAlert.UID = ""
|
|
}
|
|
|
|
interval := cfg.BaseInterval
|
|
if testCase.interval != nil {
|
|
interval = *testCase.interval
|
|
}
|
|
|
|
lim := limits
|
|
if testCase.limits != nil {
|
|
lim = *testCase.limits
|
|
}
|
|
|
|
_, err := validateRuleNode(r, "", interval, orgId, folder.UID, lim)
|
|
require.Error(t, err)
|
|
if testCase.expErr != "" {
|
|
require.ErrorContains(t, err, testCase.expErr)
|
|
}
|
|
if testCase.assert != nil {
|
|
testCase.assert(t, r, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRuleNode_UID(t *testing.T) {
|
|
orgId := rand.Int63()
|
|
folder := randFolder()
|
|
name := util.GenerateShortUID()
|
|
var cfg = config(t)
|
|
limits := makeLimits(cfg)
|
|
interval := cfg.BaseInterval * time.Duration(rand.Int63n(10)+1)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
rule func() *apimodels.PostableExtendedRuleNode
|
|
assert func(t *testing.T, model *apimodels.PostableExtendedRuleNode, rule *models.AlertRule)
|
|
}{
|
|
{
|
|
name: "use empty Title",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Title = ""
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, "", alert.Title)
|
|
},
|
|
},
|
|
{
|
|
name: "use empty NoData if NoDataState is empty",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.NoDataState = ""
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, models.NoDataState(""), alert.NoDataState)
|
|
},
|
|
},
|
|
{
|
|
name: "use empty Alerting if ExecErrState is empty",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.ExecErrState = ""
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, models.ExecutionErrorState(""), alert.ExecErrState)
|
|
},
|
|
},
|
|
{
|
|
name: "use empty Condition and Data if they are empty",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Condition = ""
|
|
r.GrafanaManagedAlert.Data = nil
|
|
if rand.Int63()%2 == 0 {
|
|
r.GrafanaManagedAlert.Data = make([]apimodels.AlertQuery, 0)
|
|
}
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, "", alert.Condition)
|
|
require.Len(t, alert.Data, 0)
|
|
},
|
|
},
|
|
{
|
|
name: "extracts Dashboard UID and Panel Id from annotations",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: util.GenerateShortUID(),
|
|
models.PanelIDAnnotation: strconv.Itoa(rand.Int()),
|
|
}
|
|
return &r
|
|
},
|
|
assert: func(t *testing.T, api *apimodels.PostableExtendedRuleNode, alert *models.AlertRule) {
|
|
require.Equal(t, api.ApiRuleNode.Annotations[models.DashboardUIDAnnotation], *alert.DashboardUID)
|
|
panelId, err := strconv.Atoi(api.ApiRuleNode.Annotations[models.PanelIDAnnotation])
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(panelId), *alert.PanelID)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
r := testCase.rule()
|
|
alert, err := validateRuleNode(r, name, interval, orgId, folder.UID, limits)
|
|
require.NoError(t, err)
|
|
testCase.assert(t, r, alert)
|
|
})
|
|
}
|
|
|
|
t.Run("accepts empty group name", func(t *testing.T) {
|
|
r := validRule()
|
|
alert, err := validateRuleNode(&r, "", interval, orgId, folder.UID, limits)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", alert.RuleGroup)
|
|
})
|
|
}
|
|
|
|
func TestValidateRuleNodeFailures_UID(t *testing.T) {
|
|
orgId := rand.Int63()
|
|
folder := randFolder()
|
|
cfg := config(t)
|
|
limits := makeLimits(cfg)
|
|
|
|
testCases := []struct {
|
|
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",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert = nil
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if title is too long",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
for len(r.GrafanaManagedAlert.Title) < store.AlertRuleMaxTitleLength {
|
|
r.GrafanaManagedAlert.Title += r.GrafanaManagedAlert.Title
|
|
}
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if there are not data (nil) but condition is set",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Data = nil
|
|
r.GrafanaManagedAlert.Condition = "A"
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if there are not data (empty) but condition is set",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.Data = make([]apimodels.AlertQuery, 0, 1)
|
|
r.GrafanaManagedAlert.Condition = "A"
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if Dashboard UID is specified but not Panel ID",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: util.GenerateShortUID(),
|
|
}
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if Dashboard UID is specified and Panel ID is NaN",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: util.GenerateShortUID(),
|
|
models.PanelIDAnnotation: util.GenerateShortUID(),
|
|
}
|
|
return &r
|
|
},
|
|
},
|
|
{
|
|
name: "fail if PanelID is specified but not Dashboard UID ",
|
|
rule: func() *apimodels.PostableExtendedRuleNode {
|
|
r := validRule()
|
|
r.ApiRuleNode.Annotations = map[string]string{
|
|
models.PanelIDAnnotation: "0",
|
|
}
|
|
return &r
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
r := testCase.rule()
|
|
|
|
interval := cfg.BaseInterval
|
|
if testCase.interval != nil {
|
|
interval = *testCase.interval
|
|
}
|
|
|
|
_, err := validateRuleNode(r, "", interval, orgId, folder.UID, limits)
|
|
require.Error(t, err)
|
|
if testCase.assert != nil {
|
|
testCase.assert(t, r, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRuleNodeIntervalFailures(t *testing.T) {
|
|
cfg := config(t)
|
|
limits := makeLimits(cfg)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
interval time.Duration
|
|
}{
|
|
{
|
|
name: "fail if interval is negative",
|
|
interval: -time.Duration(rand.Int63n(10)+1) * time.Second,
|
|
},
|
|
{
|
|
name: "fail if interval is 0",
|
|
interval: 0,
|
|
},
|
|
{
|
|
name: "fail if interval is not multiple of base interval",
|
|
interval: cfg.BaseInterval + time.Duration(rand.Int63n(int64(cfg.BaseInterval.Seconds())-2)+1)*time.Second,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
r := validRule()
|
|
_, err := validateRuleNode(&r, util.GenerateShortUID(), testCase.interval, rand.Int63(), randFolder().UID, limits)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRuleNodeNotificationSettings(t *testing.T) {
|
|
cfg := config(t)
|
|
limits := makeLimits(cfg)
|
|
|
|
validNotificationSettings := models.NotificationSettingsGen(models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel))
|
|
|
|
testCases := []struct {
|
|
name string
|
|
notificationSettings models.NotificationSettings
|
|
expErrorContains string
|
|
}{
|
|
{
|
|
name: "valid notification settings",
|
|
notificationSettings: validNotificationSettings(),
|
|
},
|
|
{
|
|
name: "missing receiver is invalid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithReceiver("")),
|
|
expErrorContains: "receiver",
|
|
},
|
|
{
|
|
name: "group by empty is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy()),
|
|
},
|
|
{
|
|
name: "group by ... is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy("...")),
|
|
},
|
|
{
|
|
name: "group by with alert name and folder name labels is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel)),
|
|
},
|
|
{
|
|
name: "group by missing alert name label is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(models.FolderTitleLabel)),
|
|
},
|
|
{
|
|
name: "group by missing folder name label is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(model.AlertNameLabel)),
|
|
},
|
|
{
|
|
name: "group wait empty is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(nil)),
|
|
},
|
|
{
|
|
name: "group wait positive is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(util.Pointer(1*time.Second))),
|
|
},
|
|
{
|
|
name: "group wait negative is invalid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(util.Pointer(-1*time.Second))),
|
|
expErrorContains: "group wait",
|
|
},
|
|
{
|
|
name: "group interval empty is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(nil)),
|
|
},
|
|
{
|
|
name: "group interval positive is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Second))),
|
|
},
|
|
{
|
|
name: "group interval negative is invalid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(util.Pointer(-1*time.Second))),
|
|
expErrorContains: "group interval",
|
|
},
|
|
{
|
|
name: "repeat interval empty is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(nil)),
|
|
},
|
|
{
|
|
name: "repeat interval positive is valid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(util.Pointer(1*time.Second))),
|
|
},
|
|
{
|
|
name: "repeat interval negative is invalid",
|
|
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(util.Pointer(-1*time.Second))),
|
|
expErrorContains: "repeat interval",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := validRule()
|
|
r.GrafanaManagedAlert.NotificationSettings = AlertRuleNotificationSettingsFromNotificationSettings([]models.NotificationSettings{tt.notificationSettings})
|
|
_, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder().UID, limits)
|
|
|
|
if tt.expErrorContains != "" {
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, tt.expErrorContains)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRuleNodeReservedLabels(t *testing.T) {
|
|
cfg := config(t)
|
|
limits := makeLimits(cfg)
|
|
|
|
for label := range models.LabelsUserCannotSpecify {
|
|
t.Run(label, func(t *testing.T) {
|
|
r := validRule()
|
|
r.ApiRuleNode.Labels = map[string]string{
|
|
label: "true",
|
|
}
|
|
_, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder().UID, limits)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, label)
|
|
})
|
|
}
|
|
}
|