Alerting: Support for Mimir configuration in Grafana Alertmanager (#106402)
This commit is contained in:
@@ -3,13 +3,17 @@ package definitions
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
alertingTemplates "github.com/grafana/alerting/templates"
|
||||
amv2 "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/grafana/alerting/definition"
|
||||
@@ -263,6 +267,7 @@ type (
|
||||
PostableApiReceiver = definition.PostableApiReceiver
|
||||
PostableGrafanaReceivers = definition.PostableGrafanaReceivers
|
||||
ReceiverType = definition.ReceiverType
|
||||
MergeResult = definition.MergeResult
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -643,13 +648,82 @@ type DatasourceUIDReference struct {
|
||||
DatasourceUID string
|
||||
}
|
||||
|
||||
type ExtraConfiguration struct {
|
||||
Identifier string `yaml:"identifier" json:"identifier"`
|
||||
MergeMatchers config.Matchers `yaml:"merge_matchers" json:"merge_matchers"`
|
||||
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
|
||||
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
||||
}
|
||||
|
||||
func (c ExtraConfiguration) Validate() error {
|
||||
if c.Identifier == "" {
|
||||
return errors.New("identifier is required")
|
||||
}
|
||||
if len(c.MergeMatchers) == 0 {
|
||||
return errors.New("at least one matcher is required")
|
||||
}
|
||||
for _, m := range c.MergeMatchers {
|
||||
if m.Type != labels.MatchEqual {
|
||||
return errors.New("only matchers with type equal are supported")
|
||||
}
|
||||
}
|
||||
err := c.AlertmanagerConfig.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid alertmanager configuration: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type PostableUserConfig struct {
|
||||
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
|
||||
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
||||
ExtraConfigs []ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"`
|
||||
amSimple map[string]interface{} `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (c *PostableUserConfig) GetMergedAlertmanagerConfig() (MergeResult, error) {
|
||||
if len(c.ExtraConfigs) == 0 {
|
||||
return MergeResult{
|
||||
Config: c.AlertmanagerConfig,
|
||||
}, nil
|
||||
}
|
||||
// support only one config for now
|
||||
mimirCfg := c.ExtraConfigs[0]
|
||||
opts := definition.MergeOpts{
|
||||
DedupSuffix: mimirCfg.Identifier,
|
||||
SubtreeMatchers: mimirCfg.MergeMatchers,
|
||||
}
|
||||
if err := opts.Validate(); err != nil {
|
||||
return MergeResult{}, fmt.Errorf("invalid merge options: %w", err)
|
||||
}
|
||||
return definition.Merge(c.AlertmanagerConfig, mimirCfg.AlertmanagerConfig, opts) // for now support only the first extra config
|
||||
}
|
||||
|
||||
// GetMergedTemplateDefinitions converts the given PostableUserConfig's TemplateFiles to a slice of TemplateDefinitions.
|
||||
func (c *PostableUserConfig) GetMergedTemplateDefinitions() []alertingTemplates.TemplateDefinition {
|
||||
out := make([]alertingTemplates.TemplateDefinition, 0, len(c.TemplateFiles))
|
||||
for name, tmpl := range c.TemplateFiles {
|
||||
out = append(out, alertingTemplates.TemplateDefinition{
|
||||
Name: name,
|
||||
Template: tmpl,
|
||||
Kind: alertingTemplates.GrafanaKind,
|
||||
})
|
||||
}
|
||||
if len(c.ExtraConfigs) == 0 {
|
||||
return out
|
||||
}
|
||||
// support only one config for now
|
||||
for name, tmpl := range c.ExtraConfigs[0].TemplateFiles {
|
||||
out = append(out, alertingTemplates.TemplateDefinition{
|
||||
Name: name,
|
||||
Template: tmpl,
|
||||
Kind: alertingTemplates.MimirKind,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *PostableUserConfig) UnmarshalJSON(b []byte) error {
|
||||
type plain PostableUserConfig
|
||||
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
|
||||
@@ -661,6 +735,15 @@ func (c *PostableUserConfig) UnmarshalJSON(b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(c.ExtraConfigs) > 1 {
|
||||
return errors.New("only one extra config is supported")
|
||||
}
|
||||
for _, extraConfig := range c.ExtraConfigs {
|
||||
if err := extraConfig.Validate(); err != nil {
|
||||
return fmt.Errorf("extra configuration is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
type intermediate struct {
|
||||
AlertmanagerConfig map[string]interface{} `yaml:"alertmanager_config" json:"alertmanager_config"`
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
alertingTemplates "github.com/grafana/alerting/templates"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -215,3 +217,201 @@ func Test_RawMessageMarshaling(t *testing.T) {
|
||||
assert.Equal(t, RawMessage(`{"data":"test"}`), n.Field)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostableUserConfig_GetMergedAlertmanagerConfig(t *testing.T) {
|
||||
alertmanagerCfg := PostableApiAlertingConfig{
|
||||
Config: Config{
|
||||
Route: &Route{
|
||||
Receiver: "default",
|
||||
},
|
||||
},
|
||||
Receivers: []*PostableApiReceiver{
|
||||
{
|
||||
Receiver: config.Receiver{
|
||||
Name: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
config PostableUserConfig
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "no extra configs",
|
||||
config: PostableUserConfig{
|
||||
AlertmanagerConfig: alertmanagerCfg,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid mimir config",
|
||||
config: PostableUserConfig{
|
||||
AlertmanagerConfig: alertmanagerCfg,
|
||||
ExtraConfigs: []ExtraConfiguration{
|
||||
{
|
||||
Identifier: "mimir-1",
|
||||
MergeMatchers: config.Matchers{
|
||||
{
|
||||
Type: labels.MatchEqual,
|
||||
Name: "cluster",
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
AlertmanagerConfig: PostableApiAlertingConfig{
|
||||
Config: Config{
|
||||
Route: &Route{
|
||||
Receiver: "mimir-receiver",
|
||||
},
|
||||
},
|
||||
Receivers: []*PostableApiReceiver{
|
||||
{
|
||||
Receiver: config.Receiver{
|
||||
Name: "mimir-receiver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty identifier",
|
||||
config: PostableUserConfig{
|
||||
AlertmanagerConfig: alertmanagerCfg,
|
||||
ExtraConfigs: []ExtraConfiguration{
|
||||
{
|
||||
Identifier: "",
|
||||
MergeMatchers: config.Matchers{},
|
||||
AlertmanagerConfig: PostableApiAlertingConfig{
|
||||
Config: Config{
|
||||
Route: &Route{
|
||||
Receiver: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "invalid merge options",
|
||||
},
|
||||
{
|
||||
name: "bad matcher type",
|
||||
config: PostableUserConfig{
|
||||
AlertmanagerConfig: alertmanagerCfg,
|
||||
ExtraConfigs: []ExtraConfiguration{
|
||||
{
|
||||
Identifier: "test",
|
||||
MergeMatchers: config.Matchers{
|
||||
{
|
||||
Type: labels.MatchNotEqual,
|
||||
Name: "cluster",
|
||||
Value: "prod",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "only equality matchers are allowed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := tc.config.GetMergedAlertmanagerConfig()
|
||||
if tc.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tc.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result.Config)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostableUserConfig_GetMergedTemplateDefinitions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config PostableUserConfig
|
||||
expectedTemplates int
|
||||
}{
|
||||
{
|
||||
name: "no templates",
|
||||
config: PostableUserConfig{
|
||||
TemplateFiles: map[string]string{},
|
||||
ExtraConfigs: []ExtraConfiguration{},
|
||||
},
|
||||
expectedTemplates: 0,
|
||||
},
|
||||
{
|
||||
name: "grafana templates only",
|
||||
config: PostableUserConfig{
|
||||
TemplateFiles: map[string]string{
|
||||
"grafana-template1": "{{ define \"test\" }}Hello{{ end }}",
|
||||
"grafana-template2": "{{ define \"test2\" }}World{{ end }}",
|
||||
},
|
||||
ExtraConfigs: []ExtraConfiguration{},
|
||||
},
|
||||
expectedTemplates: 2,
|
||||
},
|
||||
{
|
||||
name: "mimir templates only",
|
||||
config: PostableUserConfig{
|
||||
TemplateFiles: map[string]string{},
|
||||
ExtraConfigs: []ExtraConfiguration{
|
||||
{
|
||||
TemplateFiles: map[string]string{
|
||||
"mimir-template": "{{ define \"mimir\" }}Mimir{{ end }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTemplates: 1,
|
||||
},
|
||||
{
|
||||
name: "mixed templates",
|
||||
config: PostableUserConfig{
|
||||
TemplateFiles: map[string]string{
|
||||
"grafana-template": "{{ define \"grafana\" }}Grafana{{ end }}",
|
||||
},
|
||||
ExtraConfigs: []ExtraConfiguration{
|
||||
{
|
||||
TemplateFiles: map[string]string{
|
||||
"mimir-template": "{{ define \"mimir\" }}Mimir{{ end }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTemplates: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := tc.config.GetMergedTemplateDefinitions()
|
||||
require.Len(t, result, tc.expectedTemplates)
|
||||
|
||||
templateMap := make(map[string]string)
|
||||
kindMap := make(map[string]alertingTemplates.Kind)
|
||||
for _, tmpl := range result {
|
||||
templateMap[tmpl.Name] = tmpl.Template
|
||||
kindMap[tmpl.Name] = tmpl.Kind
|
||||
}
|
||||
|
||||
for name, content := range tc.config.TemplateFiles {
|
||||
require.Equal(t, content, templateMap[name])
|
||||
require.Equal(t, alertingTemplates.GrafanaKind, kindMap[name])
|
||||
}
|
||||
|
||||
if len(tc.config.ExtraConfigs) > 0 {
|
||||
for name, content := range tc.config.ExtraConfigs[0].TemplateFiles {
|
||||
require.Equal(t, content, templateMap[name])
|
||||
require.Equal(t, alertingTemplates.MimirKind, kindMap[name])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user