diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index 31bc9db1187..d5460a83e26 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -98,10 +98,8 @@ func NewConverter(cfg Config) (*Converter, error) { // PrometheusRulesToGrafana converts a Prometheus rule group into Grafana Alerting rule group. func (p *Converter) PrometheusRulesToGrafana(orgID int64, namespaceUID string, group PrometheusRuleGroup) (*models.AlertRuleGroup, error) { - for _, rule := range group.Rules { - if err := rule.Validate(); err != nil { - return nil, err - } + if err := group.Validate(); err != nil { + return nil, err } grafanaGroup, err := p.convertRuleGroup(orgID, namespaceUID, group) diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 4fcf9f42e4d..a6af11d1cc9 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -28,6 +28,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { promGroup PrometheusRuleGroup config Config expectError bool + errorMsg string }{ { name: "valid rule group", @@ -68,6 +69,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) { }, }, expectError: true, + errorMsg: "keep_firing_for is not supported", }, { name: "rule group with empty interval", @@ -100,6 +102,63 @@ func TestPrometheusRulesToGrafana(t *testing.T) { }, expectError: false, }, + { + name: "rule group with query_offset is not supported", + orgID: 1, + namespace: "namespaceUID", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + QueryOffset: func() *prommodel.Duration { + d := prommodel.Duration(30 * time.Second) + return &d + }(), + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "up == 0", + }, + }, + }, + expectError: true, + errorMsg: "query_offset is not supported", + }, + { + name: "rule group with limit is not supported", + orgID: 1, + namespace: "namespaceUID", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + Limit: 5, + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "up == 0", + }, + }, + }, + expectError: true, + errorMsg: "limit is not supported", + }, + { + name: "rule group with labels is not supported", + orgID: 1, + namespace: "namespaceUID", + promGroup: PrometheusRuleGroup{ + Name: "test-group-1", + Interval: prommodel.Duration(10 * time.Second), + Labels: map[string]string{"team": "devops"}, + Rules: []PrometheusRule{ + { + Alert: "alert-1", + Expr: "up == 0", + }, + }, + }, + expectError: true, + errorMsg: "labels are not supported", + }, } for _, tc := range testCases { @@ -114,6 +173,9 @@ func TestPrometheusRulesToGrafana(t *testing.T) { if tc.expectError { require.Error(t, err, tc.name) + if tc.errorMsg != "" { + require.Contains(t, err.Error(), tc.errorMsg, tc.name) + } return } require.NoError(t, err, tc.name) diff --git a/pkg/services/ngalert/prom/models.go b/pkg/services/ngalert/prom/models.go index cbf57b2015c..6cde9b7badd 100644 --- a/pkg/services/ngalert/prom/models.go +++ b/pkg/services/ngalert/prom/models.go @@ -7,7 +7,8 @@ import ( ) var ( - ErrPrometheusRuleValidationFailed = errutil.ValidationFailed("alerting.prometheusRuleInvalid") + ErrPrometheusRuleValidationFailed = errutil.ValidationFailed("alerting.prometheusRuleInvalid") + ErrPrometheusRuleGroupValidationFailed = errutil.ValidationFailed("alerting.prometheusRuleGroupInvalid") ) type PrometheusRulesFile struct { @@ -15,9 +16,34 @@ type PrometheusRulesFile struct { } type PrometheusRuleGroup struct { - Name string `yaml:"name"` - Interval prommodel.Duration `yaml:"interval"` - Rules []PrometheusRule `yaml:"rules"` + Name string `yaml:"name"` + Interval prommodel.Duration `yaml:"interval"` + QueryOffset *prommodel.Duration `yaml:"query_offset,omitempty"` + Limit int `yaml:"limit,omitempty"` + Rules []PrometheusRule `yaml:"rules"` + Labels map[string]string `yaml:"labels,omitempty"` +} + +func (g *PrometheusRuleGroup) Validate() error { + if g.QueryOffset != nil { + return ErrPrometheusRuleGroupValidationFailed.Errorf("query_offset is not supported") + } + + if g.Limit != 0 { + return ErrPrometheusRuleGroupValidationFailed.Errorf("limit is not supported") + } + + if len(g.Labels) > 0 { + return ErrPrometheusRuleGroupValidationFailed.Errorf("labels are not supported") + } + + for _, rule := range g.Rules { + if err := rule.Validate(); err != nil { + return err + } + } + + return nil } type PrometheusRule struct { diff --git a/pkg/services/ngalert/prom/models_test.go b/pkg/services/ngalert/prom/models_test.go index fbf7d65a847..00b6f0ac56b 100644 --- a/pkg/services/ngalert/prom/models_test.go +++ b/pkg/services/ngalert/prom/models_test.go @@ -7,8 +7,140 @@ import ( prommodel "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + + "github.com/grafana/grafana/pkg/util" ) +func TestPrometheusRuleGroup_Validate(t *testing.T) { + tests := []struct { + name string + group PrometheusRuleGroup + expectError bool + errorMsg string + }{ + { + name: "valid group with no unsupported fields", + group: PrometheusRuleGroup{ + Name: "test_group", + Interval: prommodel.Duration(60), + Rules: []PrometheusRule{ + { + Alert: "test_alert", + Expr: "up == 0", + }, + }, + }, + expectError: false, + }, + { + name: "invalid group with query_offset", + group: PrometheusRuleGroup{ + Name: "test_group", + Interval: prommodel.Duration(60), + QueryOffset: util.Pointer(prommodel.Duration(10)), + }, + expectError: true, + errorMsg: "query_offset is not supported", + }, + { + name: "invalid group with limit", + group: PrometheusRuleGroup{ + Name: "test_group", + Interval: prommodel.Duration(60), + Limit: 10, + }, + expectError: true, + errorMsg: "limit is not supported", + }, + { + name: "invalid group with labels", + group: PrometheusRuleGroup{ + Name: "test_group", + Interval: prommodel.Duration(60), + Labels: map[string]string{"foo": "bar"}, + }, + expectError: true, + errorMsg: "labels are not supported", + }, + { + name: "invalid group with invalid rule", + group: PrometheusRuleGroup{ + Name: "test_group", + Interval: prommodel.Duration(60), + Rules: []PrometheusRule{ + { + Alert: "test_alert", + Expr: "up == 0", + KeepFiringFor: util.Pointer(prommodel.Duration(10)), + }, + }, + }, + expectError: true, + errorMsg: "keep_firing_for is not supported", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.group.Validate() + if tt.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestPrometheusRule_Validate(t *testing.T) { + tests := []struct { + name string + rule PrometheusRule + expectError bool + errorMsg string + }{ + { + name: "valid alert rule", + rule: PrometheusRule{ + Alert: "test_alert", + Expr: "up == 0", + }, + expectError: false, + }, + { + name: "valid recording rule", + rule: PrometheusRule{ + Record: "test_recording", + Expr: "sum(up)", + }, + expectError: false, + }, + { + name: "invalid rule with keep_firing_for", + rule: PrometheusRule{ + Alert: "test_alert", + Expr: "up == 0", + KeepFiringFor: util.Pointer(prommodel.Duration(10)), + }, + expectError: true, + errorMsg: "keep_firing_for is not supported", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.rule.Validate() + if tt.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMsg) + } else { + require.NoError(t, err) + } + }) + } +} + func TestPrometheusRulesFileYAML(t *testing.T) { interval := prommodel.Duration(5 * time.Minute) alertFor := prommodel.Duration(10 * time.Minute)