Alerting: Separate configuration model for remote Alertmanager Mimir client (#107741)

* replace PostableUserConfig with GrafanaAlertmanagerConfig to decouple from internal Grafana models
* update alertmanager + tests
* calculate hash of the GrafanaAlertmanagerConfig
This commit is contained in:
Yuri Tseretyan
2025-07-09 12:42:10 -04:00
committed by GitHub
parent 0e253721b0
commit 4bb6926eee
6 changed files with 128 additions and 23 deletions
+19 -10
View File
@@ -293,18 +293,20 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config
if err := am.mergeExtraConfigs(ctx, decryptedCfg); err != nil {
return fmt.Errorf("unable to merge extra configurations: %w", err)
}
rawDecrypted, err := json.Marshal(decryptedCfg)
payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg)
rawPayload, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("unable to marshal decrypted configuration: %w", err)
}
configHash := md5.Sum(rawDecrypted)
configHash := md5.Sum(rawPayload)
// Send the configuration only if we need to.
if !am.shouldSendConfig(ctx, configHash) {
return nil
}
return am.sendConfiguration(ctx, decryptedCfg, fmt.Sprintf("%x", configHash), config.CreatedAt, am.isDefaultConfiguration(configHash))
return am.sendConfiguration(ctx, payload, fmt.Sprintf("%x", configHash), config.CreatedAt, am.isDefaultConfiguration(configHash))
}
func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool {
@@ -370,11 +372,11 @@ func (am *Alertmanager) mergeExtraConfigs(ctx context.Context, config *apimodels
return nil
}
func (am *Alertmanager) sendConfiguration(ctx context.Context, decrypted *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error {
func (am *Alertmanager) sendConfiguration(ctx context.Context, cfg *remoteClient.GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error {
am.metrics.ConfigSyncsTotal.Inc()
if err := am.mimirClient.CreateGrafanaAlertmanagerConfig(
ctx,
decrypted,
cfg,
hash,
createdAt,
isDefault,
@@ -420,14 +422,14 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P
if err := am.mergeExtraConfigs(ctx, decryptedCfg); err != nil {
return fmt.Errorf("unable to merge extra configurations: %w", err)
}
rawCfg, err := json.Marshal(decryptedCfg)
payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg)
rawCfg, err := json.Marshal(payload)
if err != nil {
return err
}
hash := fmt.Sprintf("%x", md5.Sum(rawCfg))
return am.sendConfiguration(ctx, decryptedCfg, hash, time.Now().Unix(), false)
return am.sendConfiguration(ctx, payload, hash, time.Now().Unix(), false)
}
// SaveAndApplyDefaultConfig sends the default Grafana Alertmanager configuration to the remote Alertmanager.
@@ -446,10 +448,17 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error {
return err
}
payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg)
rawCfg, err := json.Marshal(payload)
if err != nil {
return err
}
hash := fmt.Sprintf("%x", md5.Sum(rawCfg))
return am.sendConfiguration(
ctx,
decryptedCfg,
am.defaultConfigHash,
payload,
hash,
time.Now().Unix(),
true,
)
@@ -3,6 +3,7 @@ package remote
import (
"context"
"crypto/md5"
"embed"
"encoding/base64"
"encoding/json"
"errors"
@@ -11,6 +12,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"path"
"slices"
"strings"
"testing"
@@ -47,6 +49,9 @@ import (
"github.com/grafana/grafana/pkg/util"
)
//go:embed test-data/*.*
var testData embed.FS
var (
defaultGrafanaConfig = setting.GetAlertmanagerDefaultConfiguration()
errTest = errors.New("test")
@@ -357,12 +362,14 @@ func TestCompareAndSendConfiguration(t *testing.T) {
testGrafanaConfigWithBadEncryption, err := json.Marshal(inputCfg)
require.NoError(t, err)
cfgWithDecryptedSecret, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
test, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
require.NoError(t, err)
cfgWithDecryptedSecret := PostableUserConfigToGrafanaAlertmanagerConfig(test)
cfgWithAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
testAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret))
require.NoError(t, err)
require.NoError(t, testAutogenFn(nil, nil, 0, &cfgWithAutogenRoutes.AlertmanagerConfig, false))
require.NoError(t, testAutogenFn(nil, nil, 0, &testAutogenRoutes.AlertmanagerConfig, false))
cfgWithAutogenRoutes := PostableUserConfigToGrafanaAlertmanagerConfig(testAutogenRoutes)
// Calculate hashes for expected configurations
cfgWithDecryptedSecretBytes, err := json.Marshal(cfgWithDecryptedSecret)
@@ -373,6 +380,20 @@ func TestCompareAndSendConfiguration(t *testing.T) {
require.NoError(t, err)
cfgWithAutogenRoutesHash := fmt.Sprintf("%x", md5.Sum(cfgWithAutogenRoutesBytes))
cfgWithExtraUnmergedBytes, err := testData.ReadFile(path.Join("test-data", "config-with-extra.json"))
require.NoError(t, err)
cfgWithExtraUnmerged, err := notifier.Load(cfgWithExtraUnmergedBytes)
require.NoError(t, err)
r, err := cfgWithExtraUnmerged.GetMergedAlertmanagerConfig()
require.NoError(t, err)
cfgWithExtraMerged := &client.GrafanaAlertmanagerConfig{
TemplateFiles: cfgWithExtraUnmerged.TemplateFiles,
AlertmanagerConfig: r.Config,
}
cfgWithExtraMergedBytes, err := json.Marshal(cfgWithExtraMerged)
require.NoError(t, err)
cfgWithExtraMergedHash := fmt.Sprintf("%x", md5.Sum(cfgWithExtraMergedBytes))
tests := []struct {
name string
config string
@@ -428,6 +449,15 @@ func TestCompareAndSendConfiguration(t *testing.T) {
},
nil,
},
{
name: "no error, with extra configurations",
config: string(cfgWithExtraUnmergedBytes),
autogenFn: NoopAutogenFn,
expCfg: &client.UserGrafanaConfig{
GrafanaAlertmanagerConfig: cfgWithExtraMerged,
Hash: cfgWithExtraMergedHash,
},
},
}
for _, test := range tests {
@@ -677,7 +707,7 @@ func TestCompareAndSendConfigurationWithExtraConfigs(t *testing.T) {
// Return an empty config to ensure it gets replaced
w.Header().Add("content-type", "application/json")
require.NoError(t, json.NewEncoder(w).Encode(client.UserGrafanaConfig{
GrafanaAlertmanagerConfig: &apimodels.PostableUserConfig{},
GrafanaAlertmanagerConfig: &client.GrafanaAlertmanagerConfig{},
}))
return
}
@@ -7,6 +7,7 @@ import (
"net/http"
"github.com/grafana/alerting/definition"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
@@ -15,14 +16,25 @@ const (
grafanaAlertmanagerReceiversPath = "/api/v1/grafana/receivers"
)
type GrafanaAlertmanagerConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig definition.PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
}
func (u *GrafanaAlertmanagerConfig) MarshalJSON() ([]byte, error) {
// this is special marshaling that makes sure that secrets are not masked
type cfg GrafanaAlertmanagerConfig
return definition.MarshalJSONWithSecrets((*cfg)(u))
}
type UserGrafanaConfig struct {
GrafanaAlertmanagerConfig *apimodels.PostableUserConfig `json:"configuration"`
Hash string `json:"configuration_hash"`
CreatedAt int64 `json:"created"`
Default bool `json:"default"`
Promoted bool `json:"promoted"`
ExternalURL string `json:"external_url"`
SmtpConfig SmtpConfig `json:"smtp_config"`
GrafanaAlertmanagerConfig *GrafanaAlertmanagerConfig `json:"configuration"`
Hash string `json:"configuration_hash"`
CreatedAt int64 `json:"created"`
Default bool `json:"default"`
Promoted bool `json:"promoted"`
ExternalURL string `json:"external_url"`
SmtpConfig SmtpConfig `json:"smtp_config"`
// TODO: Remove once everything can be sent in the 'SmtpConfig' field.
SmtpFrom string `json:"smtp_from"`
@@ -52,7 +64,7 @@ func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafana
return gc, nil
}
func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error {
func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error {
payload, err := definition.MarshalJSONWithSecrets(&UserGrafanaConfig{
GrafanaAlertmanagerConfig: cfg,
Hash: hash,
+1 -1
View File
@@ -29,7 +29,7 @@ type MimirClient interface {
DeleteGrafanaAlertmanagerState(ctx context.Context) error
GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error)
CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error
CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error
DeleteGrafanaAlertmanagerConfig(ctx context.Context) error
TestTemplate(ctx context.Context, c alertingNotify.TestTemplatesConfigBodyParams) (*alertingNotify.TestTemplatesResults, error)
+13
View File
@@ -0,0 +1,13 @@
package remote
import (
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/remote/client"
)
func PostableUserConfigToGrafanaAlertmanagerConfig(config *definitions.PostableUserConfig) *client.GrafanaAlertmanagerConfig {
return &client.GrafanaAlertmanagerConfig{
TemplateFiles: config.TemplateFiles,
AlertmanagerConfig: config.AlertmanagerConfig,
}
}
@@ -0,0 +1,41 @@
{
"template_files": {
"test": "{{ define \"my_templ\" }}TEST{{ end }}"
},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email",
"group_by": [
"grafana_folder",
"alertname"
]
},
"receivers": [
{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [
{
"uid": "",
"name": "some other name",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses": "<example@email.com>"
}
}
]
}
]
},
"extra_config": [
{
"identifier": "imported",
"merge_matchers": ["imported=\"true\""],
"template_files":
{
"extra_template": "{{ define \"my_message\" }}TEST{{ end }}"
},
"alertmanager_config": "{\"receivers\":[{\"webhook_configs\":[{\"url\":\"http://localhost\"}],\"name\":\"webhook\"}],\"route\":{\"receiver\":\"webhook\"}}"
}
]
}