Remote Alertmanager: Send SMTP config (#106337)
Actionlint / Lint GitHub Actions files (push) Waiting to run Details
Backend Code Checks / Validate Backend Configs (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
CodeQL checks / Analyze (actions) (push) Waiting to run Details
CodeQL checks / Analyze (go) (push) Waiting to run Details
CodeQL checks / Analyze (javascript) (push) Waiting to run Details
Lint Frontend / Verify i18n (push) Waiting to run Details
Lint Frontend / Lint (push) Waiting to run Details
Lint Frontend / Typecheck (push) Waiting to run Details
Lint Frontend / Betterer (push) Waiting to run Details
golangci-lint / lint-go (push) Waiting to run Details
Documentation / Build & Verify Docs (push) Waiting to run Details
End-to-end tests / Build & Package Grafana (push) Waiting to run Details
End-to-end tests / Build E2E test runner (push) Waiting to run Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/dashboards-suite, dashboards-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/panels-suite, panels-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/smoke-tests-suite, smoke-tests-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/various-suite, various-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/dashboards-suite, dashboards-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/panels-suite, panels-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/smoke-tests-suite, smoke-tests-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/various-suite, various-suite) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (1) (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (2) (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (3) (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (4) (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (5) (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (6) (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (7) (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
publish-technical-documentation-next / sync (push) Waiting to run Details
Reject GitHub secrets / reject-gh-secrets (push) Waiting to run Details
Run dashboard schema v2 e2e / dashboard-schema-v2-e2e (push) Waiting to run Details
Verify Storybook / Verify Storybook (push) Waiting to run Details
Dispatch sync to mirror / dispatch-job (push) Waiting to run Details
Crowdin Upload Action / upload-sources-to-crowdin (push) Has been cancelled Details
trigger-dashboard-search-e2e / trigger-search-e2e (push) Has been cancelled Details

* (WIP) Remote Alertmanager: Send SMTP config

* send SMTP configs separately

* bring back deleted fields

* actually send stuff over

* remove redundant type, fix comments

* smtp -> smtpConfig

* also send SmtpFrom an StaticHeaders separately

* tests

* restore defaults.ini
This commit is contained in:
Santiago 2025-06-13 12:44:39 -03:00 committed by GitHub
parent edd179b4ef
commit 3fe73b8de9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 112 additions and 11 deletions

View File

@ -38,6 +38,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/remote" "github.com/grafana/grafana/pkg/services/ngalert/remote"
remoteClient "github.com/grafana/grafana/pkg/services/ngalert/remote/client"
"github.com/grafana/grafana/pkg/services/ngalert/schedule" "github.com/grafana/grafana/pkg/services/ngalert/schedule"
"github.com/grafana/grafana/pkg/services/ngalert/sender" "github.com/grafana/grafana/pkg/services/ngalert/sender"
"github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state"
@ -187,15 +188,30 @@ func (ng *AlertNG) init() error {
remoteSecondary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondary) remoteSecondary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondary)
if remotePrimary || remoteSecondary { if remotePrimary || remoteSecondary {
m := ng.Metrics.GetRemoteAlertmanagerMetrics() m := ng.Metrics.GetRemoteAlertmanagerMetrics()
smtpCfg := remoteClient.SmtpConfig{
FromAddress: ng.Cfg.Smtp.FromAddress,
FromName: ng.Cfg.Smtp.FromName,
Host: ng.Cfg.Smtp.Host,
User: ng.Cfg.Smtp.User,
Password: ng.Cfg.Smtp.Password,
EhloIdentity: ng.Cfg.Smtp.EhloIdentity,
StartTLSPolicy: ng.Cfg.Smtp.StartTLSPolicy,
SkipVerify: ng.Cfg.Smtp.SkipVerify,
StaticHeaders: ng.Cfg.Smtp.StaticHeaders,
}
cfg := remote.AlertmanagerConfig{ cfg := remote.AlertmanagerConfig{
BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password, BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password,
DefaultConfig: ng.Cfg.UnifiedAlerting.DefaultConfiguration, DefaultConfig: ng.Cfg.UnifiedAlerting.DefaultConfiguration,
TenantID: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.TenantID, TenantID: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.TenantID,
URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL, URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL,
ExternalURL: ng.Cfg.AppURL, ExternalURL: ng.Cfg.AppURL,
SmtpFrom: ng.Cfg.Smtp.FromAddress, SmtpConfig: smtpCfg,
StaticHeaders: ng.Cfg.Smtp.StaticHeaders,
Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout, Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout,
// TODO: Remove once everything can be sent in the 'smtp_config' field.
SmtpFrom: ng.Cfg.Smtp.FromAddress,
StaticHeaders: ng.Cfg.Smtp.StaticHeaders,
} }
autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, skipInvalid bool) error { autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, skipInvalid bool) error {
return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, skipInvalid) return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, skipInvalid)

View File

@ -70,6 +70,8 @@ type Alertmanager struct {
amClient *remoteClient.Alertmanager amClient *remoteClient.Alertmanager
mimirClient remoteClient.MimirClient mimirClient remoteClient.MimirClient
smtp remoteClient.SmtpConfig
} }
type AlertmanagerConfig struct { type AlertmanagerConfig struct {
@ -87,15 +89,19 @@ type AlertmanagerConfig struct {
// The same flag is used for promoting state. // The same flag is used for promoting state.
PromoteConfig bool PromoteConfig bool
// SmtpFrom and StaticHeaders are used in email notifications sent by the remote Alertmanager. // SmtpConfig has all the necessary settings for the remote Alertmanager to create an email sender.
SmtpFrom string SmtpConfig remoteClient.SmtpConfig
StaticHeaders map[string]string
// SyncInterval determines how often we should attempt to synchronize configuration. // SyncInterval determines how often we should attempt to synchronize configuration.
SyncInterval time.Duration SyncInterval time.Duration
// Timeout for the HTTP client. // Timeout for the HTTP client.
Timeout time.Duration Timeout time.Duration
// TODO: Remove once everything can be send in the 'smtp_config' field.
// SmtpFrom and StaticHeaders are used in email notifications sent by the remote Alertmanager.
SmtpFrom string
StaticHeaders map[string]string
} }
func (cfg *AlertmanagerConfig) Validate() error { func (cfg *AlertmanagerConfig) Validate() error {
@ -131,6 +137,10 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto
URL: u, URL: u,
PromoteConfig: cfg.PromoteConfig, PromoteConfig: cfg.PromoteConfig,
ExternalURL: cfg.ExternalURL, ExternalURL: cfg.ExternalURL,
Smtp: cfg.SmtpConfig,
// TODO: Remove once everything can be sent in the 'smtp_config' field.
SmtpFrom: cfg.SmtpFrom, SmtpFrom: cfg.SmtpFrom,
StaticHeaders: cfg.StaticHeaders, StaticHeaders: cfg.StaticHeaders,
} }
@ -200,12 +210,15 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto
metrics: metrics, metrics: metrics,
mimirClient: mc, mimirClient: mc,
orgID: cfg.OrgID, orgID: cfg.OrgID,
smtpFrom: cfg.SmtpFrom,
state: store, state: store,
sender: s, sender: s,
syncInterval: cfg.SyncInterval, syncInterval: cfg.SyncInterval,
tenantID: cfg.TenantID, tenantID: cfg.TenantID,
url: cfg.URL, url: cfg.URL,
smtp: cfg.SmtpConfig,
// TODO: Remove once it can be sent only in the 'smtp_config' field.
smtpFrom: cfg.SmtpFrom,
}, nil }, nil
} }
@ -686,11 +699,34 @@ func (am *Alertmanager) shouldSendConfig(ctx context.Context, hash [16]byte) boo
return true return true
} }
// TODO: Remove when the from address can be sent only in the 'smtp_config' field.
if rc.SmtpFrom != am.smtpFrom { if rc.SmtpFrom != am.smtpFrom {
am.log.Debug("SMTP 'from' address is different, sending the configuration to the remote Alertmanager", "remote", rc.SmtpFrom, "local", am.smtpFrom) am.log.Debug("SMTP 'from' address is different, sending the configuration to the remote Alertmanager", "remote", rc.SmtpFrom, "local", am.smtpFrom)
return true return true
} }
// Compare SMTP configs.
if rc.SmtpConfig.EhloIdentity != am.smtp.EhloIdentity ||
rc.SmtpConfig.Password != am.smtp.Password ||
rc.SmtpConfig.FromAddress != am.smtp.FromAddress ||
rc.SmtpConfig.FromName != am.smtp.FromName ||
rc.SmtpConfig.Host != am.smtp.Host ||
rc.SmtpConfig.SkipVerify != am.smtp.SkipVerify ||
rc.SmtpConfig.StartTLSPolicy != am.smtp.StartTLSPolicy ||
len(rc.SmtpConfig.StaticHeaders) != len(am.smtp.StaticHeaders) ||
rc.SmtpConfig.User != am.smtp.User {
am.log.Debug("SMTP config is different, sending the configuration to the remote Alertmanager")
return true
}
for k, v := range rc.SmtpConfig.StaticHeaders {
if value, ok := am.smtp.StaticHeaders[k]; !ok || v != value {
am.log.Debug("SMTP static headers are different, sending the configuration to the remote Alertmanager")
return true
}
}
// Hash and compare Alertmanager configs.
rawRemote, err := json.Marshal(rc.GrafanaAlertmanagerConfig) rawRemote, err := json.Marshal(rc.GrafanaAlertmanagerConfig)
if err != nil { if err != nil {
am.log.Error("Unable to marshal the remote Alertmanager configuration for comparison", "err", err) am.log.Error("Unable to marshal the remote Alertmanager configuration for comparison", "err", err)

View File

@ -188,6 +188,10 @@ func TestApplyConfig(t *testing.T) {
PromoteConfig: true, PromoteConfig: true,
SyncInterval: 1 * time.Hour, SyncInterval: 1 * time.Hour,
ExternalURL: "https://test.grafana.com", ExternalURL: "https://test.grafana.com",
SmtpConfig: client.SmtpConfig{
FromAddress: "test-instance@grafana.net",
},
SmtpFrom: "test-instance@grafana.net", SmtpFrom: "test-instance@grafana.net",
StaticHeaders: map[string]string{"Header-1": "Value-1", "Header-2": "Value-2"}, StaticHeaders: map[string]string{"Header-1": "Value-1", "Header-2": "Value-2"},
} }
@ -259,10 +263,28 @@ func TestApplyConfig(t *testing.T) {
require.Equal(t, 3, configSyncs) require.Equal(t, 3, configSyncs)
require.Equal(t, am.smtpFrom, configSent.SmtpFrom) require.Equal(t, am.smtpFrom, configSent.SmtpFrom)
// Changing fields in the SMTP config should result in the configuration being updated.
cfg.SmtpConfig = client.SmtpConfig{
EhloIdentity: "test",
FromAddress: "test@test.com",
FromName: "Test Name",
Host: "test:25",
Password: "test",
SkipVerify: true,
StartTLSPolicy: "test",
StaticHeaders: map[string]string{"test": "true"},
User: "Test User",
}
am, err = NewAlertmanager(context.Background(), cfg, fstore, secretsService.Decrypt, NoopAutogenFn, m, tracing.InitializeTracerForTest())
require.NoError(t, err)
require.NoError(t, am.ApplyConfig(ctx, config))
require.Equal(t, 4, configSyncs)
require.Equal(t, am.smtp, configSent.SmtpConfig)
// Failing to add the auto-generated routes should result in an error. // Failing to add the auto-generated routes should result in an error.
_, err = NewAlertmanager(context.Background(), cfg, fstore, secretsService.Decrypt, errAutogenFn, m, tracing.InitializeTracerForTest()) _, err = NewAlertmanager(context.Background(), cfg, fstore, secretsService.Decrypt, errAutogenFn, m, tracing.InitializeTracerForTest())
require.ErrorIs(t, err, errTest) require.ErrorIs(t, err, errTest)
require.Equal(t, 3, configSyncs) require.Equal(t, 4, configSyncs)
} }
func TestCompareAndSendConfiguration(t *testing.T) { func TestCompareAndSendConfiguration(t *testing.T) {

View File

@ -22,8 +22,11 @@ type UserGrafanaConfig struct {
Default bool `json:"default"` Default bool `json:"default"`
Promoted bool `json:"promoted"` Promoted bool `json:"promoted"`
ExternalURL string `json:"external_url"` ExternalURL string `json:"external_url"`
SmtpFrom string `json:"smtp_from"` SmtpConfig SmtpConfig `json:"smtp_config"`
StaticHeaders map[string]string `json:"static_headers"`
// TODO: Remove once everything can be sent in the 'SmtpConfig' field.
SmtpFrom string `json:"smtp_from"`
StaticHeaders map[string]string `json:"static_headers"`
} }
func (mc *Mimir) ShouldPromoteConfig() bool { func (mc *Mimir) ShouldPromoteConfig() bool {
@ -57,8 +60,11 @@ func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimo
Default: isDefault, Default: isDefault,
Promoted: mc.promoteConfig, Promoted: mc.promoteConfig,
ExternalURL: mc.externalURL, ExternalURL: mc.externalURL,
SmtpFrom: mc.smtpFrom, SmtpConfig: mc.smtpConfig,
StaticHeaders: mc.staticHeaders,
// TODO: Remove once everything can be sent only in the 'smtp_config' field.
SmtpFrom: mc.smtpFrom,
StaticHeaders: mc.staticHeaders,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -48,10 +48,25 @@ type Mimir struct {
metrics *metrics.RemoteAlertmanager metrics *metrics.RemoteAlertmanager
promoteConfig bool promoteConfig bool
externalURL string externalURL string
smtpConfig SmtpConfig
// TODO: Remove once everything can be sent in the 'smtp' field.
smtpFrom string smtpFrom string
staticHeaders map[string]string staticHeaders map[string]string
} }
type SmtpConfig struct {
EhloIdentity string `json:"ehlo_identity"`
FromAddress string `json:"from_address"`
FromName string `json:"from_name"`
Host string `json:"host"`
Password string `json:"password"`
SkipVerify bool `json:"skip_verify"`
StartTLSPolicy string `json:"start_tls_policy"`
StaticHeaders map[string]string `json:"static_headers"`
User string `json:"user"`
}
type Config struct { type Config struct {
URL *url.URL URL *url.URL
TenantID string TenantID string
@ -60,6 +75,9 @@ type Config struct {
Logger log.Logger Logger log.Logger
PromoteConfig bool PromoteConfig bool
ExternalURL string ExternalURL string
Smtp SmtpConfig
// TODO: Remove once everything can be sent in the 'smtp_config' field.
SmtpFrom string SmtpFrom string
StaticHeaders map[string]string StaticHeaders map[string]string
} }
@ -105,6 +123,9 @@ func New(cfg *Config, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer
metrics: metrics, metrics: metrics,
promoteConfig: cfg.PromoteConfig, promoteConfig: cfg.PromoteConfig,
externalURL: cfg.ExternalURL, externalURL: cfg.ExternalURL,
smtpConfig: cfg.Smtp,
// TODO: Remove once everything can be sent in the 'smtp_config' field.
smtpFrom: cfg.SmtpFrom, smtpFrom: cfg.SmtpFrom,
staticHeaders: cfg.StaticHeaders, staticHeaders: cfg.StaticHeaders,
}, nil }, nil