From cd889fef9b4bb87c934be66de58c4b84cecd09f8 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Mon, 6 Oct 2025 09:28:38 +0200 Subject: [PATCH] Alerting: Keep extra configurations on main config update (#106958) --- .../ngalert/notifier/alertmanager_config.go | 24 ++ .../notifier/alertmanager_config_test.go | 258 ++++++++++++++++++ 2 files changed, 282 insertions(+) diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 9f101621ef8..514da9be686 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -324,6 +324,15 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyAlertmanagerConfiguration(ctx conte } cleanPermissionsErr := err + if previousConfig != nil { + // If there is a previous configuration, we need to copy its extra configs to the new one. + extraConfigs, err := extractExtraConfigs(previousConfig.AlertmanagerConfiguration) + if err != nil { + return fmt.Errorf("failed to extract extra configs from previous configuration: %w", err) + } + config.ExtraConfigs = extraConfigs + } + if err := moa.Crypto.ProcessSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers); err != nil { return fmt.Errorf("failed to post process Alertmanager configuration: %w", err) } @@ -572,3 +581,18 @@ func extractReceiverNames(rawConfig string) (sets.Set[string], error) { return receiverNames, nil } + +// extractExtraConfigs extracts encrypted (does not decrypt) extra configurations from the raw Alertmanager config. +func extractExtraConfigs(rawConfig string) ([]definitions.ExtraConfiguration, error) { + // Slimmed down version of the Alertmanager configuration to extract extra configs. + type extraConfigUserConfig struct { + ExtraConfigs []definitions.ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"` + } + + cfg := &extraConfigUserConfig{} + if err := json.Unmarshal([]byte(rawConfig), cfg); err != nil { + return nil, fmt.Errorf("unable to parse Alertmanager configuration: %w", err) + } + + return cfg.ExtraConfigs, nil +} diff --git a/pkg/services/ngalert/notifier/alertmanager_config_test.go b/pkg/services/ngalert/notifier/alertmanager_config_test.go index 3b12153da98..f4b06285db5 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_config_test.go @@ -150,6 +150,264 @@ receivers: }) } +func TestMultiOrgAlertmanager_SaveAndApplyAlertmanagerConfiguration(t *testing.T) { + orgID := int64(1) + ctx := context.Background() + + t.Run("SaveAndApplyAlertmanagerConfiguration preserves existing extra configs", func(t *testing.T) { + mam := setupMam(t, nil) + require.NoError(t, mam.LoadAndSyncAlertmanagersForOrgs(ctx)) + + extraConfig := definitions.ExtraConfiguration{ + Identifier: "test-extra-config", + MergeMatchers: amconfig.Matchers{&labels.Matcher{Type: labels.MatchEqual, Name: "env", Value: "test"}}, + TemplateFiles: map[string]string{"test.tmpl": "{{ define \"test\" }}Test{{ end }}"}, + AlertmanagerConfig: `route: + receiver: extra-receiver +receivers: + - name: extra-receiver`, + } + + err := mam.SaveAndApplyExtraConfiguration(ctx, orgID, extraConfig) + require.NoError(t, err) + + // Verify extra config was saved + gettableConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, gettableConfig.ExtraConfigs, 1) + require.Equal(t, extraConfig.Identifier, gettableConfig.ExtraConfigs[0].Identifier) + + // Apply a new main configuration + newMainConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "main-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "main-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "main-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "me@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err = mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, newMainConfig) + require.NoError(t, err) + + // Verify that the extra config is still present after applying the new main config + updatedConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, updatedConfig.ExtraConfigs, 1) + require.Equal(t, extraConfig.Identifier, updatedConfig.ExtraConfigs[0].Identifier) + require.Equal(t, extraConfig.TemplateFiles, updatedConfig.ExtraConfigs[0].TemplateFiles) + + // Verify the main config was updated + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Route.Receiver) + require.Len(t, updatedConfig.AlertmanagerConfig.Receivers, 1) + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Receivers[0].Name) + }) + + t.Run("SaveAndApplyAlertmanagerConfiguration handles missing extra_configs field", func(t *testing.T) { + mam := setupMam(t, nil) + require.NoError(t, mam.LoadAndSyncAlertmanagersForOrgs(ctx)) + + // Apply initial config without extra_configs field + initialConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "initial-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "initial-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "initial-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "initial@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err := mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, initialConfig) + require.NoError(t, err) + + // Apply a new main configuration + newMainConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "main-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "main-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "main-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "me@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err = mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, newMainConfig) + require.NoError(t, err) + + // Verify that no extra configs are present and main config was updated + updatedConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, updatedConfig.ExtraConfigs, 0) + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Route.Receiver) + }) + + t.Run("SaveAndApplyAlertmanagerConfiguration handles empty extra_configs array", func(t *testing.T) { + mam := setupMam(t, nil) + require.NoError(t, mam.LoadAndSyncAlertmanagersForOrgs(ctx)) + + // Apply initial config with empty extra_configs + initialConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "initial-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "initial-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "initial-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "initial@grafana.com"}`), + }, + }, + }, + }, + }, + }, + ExtraConfigs: []definitions.ExtraConfiguration{}, // Empty array + } + + err := mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, initialConfig) + require.NoError(t, err) + + // Apply a new main configuration + newMainConfig := definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Receiver: "main-receiver", + }, + }, + Receivers: []*definitions.PostableApiReceiver{ + { + Receiver: amconfig.Receiver{ + Name: "main-receiver", + }, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + Name: "main-receiver", + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "me@grafana.com"}`), + }, + }, + }, + }, + }, + }, + } + + err = mam.SaveAndApplyAlertmanagerConfiguration(ctx, orgID, newMainConfig) + require.NoError(t, err) + + // Verify that no extra configs are present and main config was updated + updatedConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false) + require.NoError(t, err) + require.Len(t, updatedConfig.ExtraConfigs, 0) + require.Equal(t, "main-receiver", updatedConfig.AlertmanagerConfig.Route.Receiver) + }) +} + +func TestExtractExtraConfigs(t *testing.T) { + t.Run("extracts extra configs from JSON", func(t *testing.T) { + jsonConfig := `{ + "extra_config": [ + { + "identifier": "test-config", + "merge_matchers": [], + "template_files": {"test.tmpl": "test"}, + "alertmanager_config": "route:\n receiver: test" + } + ] + }` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 1) + require.Equal(t, "test-config", extraConfigs[0].Identifier) + }) + + t.Run("handles missing extra_config field", func(t *testing.T) { + jsonConfig := `{"alertmanager_config": {"route": {"receiver": "test"}}}` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 0) + }) + + t.Run("handles empty extra_config array", func(t *testing.T) { + jsonConfig := `{"extra_config": []}` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 0) + }) + + t.Run("handles null extra_config", func(t *testing.T) { + jsonConfig := `{"extra_config": null}` + + extraConfigs, err := extractExtraConfigs(jsonConfig) + require.NoError(t, err) + require.Len(t, extraConfigs, 0) + }) +} + func TestMultiOrgAlertmanager_DeleteExtraConfiguration(t *testing.T) { orgID := int64(1)