Alerting: Return merged extra configurations in the API (#109636)

This commit is contained in:
Alexander Akhmetov
2025-08-14 07:46:12 +02:00
committed by GitHub
parent 7d7721e30a
commit e165a490b4
9 changed files with 214 additions and 146 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *contextmodel.ReqContext)
func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) response.Response {
canSeeAutogen := c.HasRole(org.RoleAdmin)
config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), canSeeAutogen)
config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), canSeeAutogen, false)
if err != nil {
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return ErrResp(http.StatusNotFound, err, "")
@@ -135,7 +135,7 @@ type ConvertPrometheusSrv struct {
type Alertmanager interface {
DeleteExtraConfiguration(ctx context.Context, org int64, identifier string) error
SaveAndApplyExtraConfiguration(ctx context.Context, org int64, extraConfig apimodels.ExtraConfiguration) error
GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool) (apimodels.GettableUserConfig, error)
GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool, withMergedExtraConfig bool) (apimodels.GettableUserConfig, error)
}
func NewConvertPrometheusSrv(
@@ -594,7 +594,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *
identifier := parseConfigIdentifierHeader(c)
cfg, err := srv.am.GetAlertmanagerConfiguration(ctx, c.GetOrgID(), false)
cfg, err := srv.am.GetAlertmanagerConfiguration(ctx, c.GetOrgID(), false, false)
if err != nil {
logger.Error("failed to get alertmanager configuration", "err", err)
return errorToResponse(err)
@@ -1782,8 +1782,8 @@ func (m *mockAlertmanager) SaveAndApplyExtraConfiguration(ctx context.Context, o
return args.Error(0)
}
func (m *mockAlertmanager) GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool) (apimodels.GettableUserConfig, error) {
args := m.Called(ctx, org, withAutogen)
func (m *mockAlertmanager) GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool, withMergedExtraConfig bool) (apimodels.GettableUserConfig, error) {
args := m.Called(ctx, org, withAutogen, withMergedExtraConfig)
return args.Get(0).(apimodels.GettableUserConfig), args.Error(1)
}
@@ -1912,7 +1912,7 @@ func TestRouteConvertPrometheusGetAlertmanagerConfig(t *testing.T) {
t.Run("without config identifier header should use default identifier", func(t *testing.T) {
mockAM := &mockAlertmanager{}
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false).Return(apimodels.GettableUserConfig{
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(apimodels.GettableUserConfig{
ExtraConfigs: []apimodels.ExtraConfiguration{
{
Identifier: defaultConfigIdentifier,
@@ -1935,7 +1935,7 @@ receivers:
t.Run("with empty config identifier header should use default identifier", func(t *testing.T) {
mockAM := &mockAlertmanager{}
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false).Return(apimodels.GettableUserConfig{
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(apimodels.GettableUserConfig{
ExtraConfigs: []apimodels.ExtraConfiguration{
{
Identifier: defaultConfigIdentifier,
@@ -1983,7 +1983,7 @@ receivers:
},
}
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, int64(1), false).Return(expectedConfig, nil).Once()
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, int64(1), false, false).Return(expectedConfig, nil).Once()
rc := createRequestCtx()
rc.Req.Header.Set(configIdentifierHeader, identifier)
@@ -2037,7 +2037,7 @@ receivers:
},
}
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false).Return(expectedConfig, nil).Once()
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(expectedConfig, nil).Once()
rc := createRequestCtx()
rc.Req.Header.Set(configIdentifierHeader, identifier)
@@ -2052,7 +2052,7 @@ receivers:
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false).Return(apimodels.GettableUserConfig{}, errors.New("config error")).Once()
mockAM.On("GetAlertmanagerConfiguration", mock.Anything, orgID, false, false).Return(apimodels.GettableUserConfig{}, errors.New("config error")).Once()
rc := createRequestCtx()
rc.Req.Header.Set(configIdentifierHeader, identifier)
@@ -1,8 +1,8 @@
package api
import (
"errors"
"net/http"
"strings"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
@@ -11,11 +11,13 @@ import (
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
const extraConfigPrefix = "~grafana-converted-extra-config-"
const withExtraConfigUID = "~grafana-with-extra-config"
type ConvertService interface {
RouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response
@@ -42,7 +44,7 @@ func NewForkingAM(datasourceCache datasources.CacheService, proxy *LotexAM, graf
func (f *AlertmanagerApiHandler) getService(ctx *contextmodel.ReqContext) (*LotexAM, error) {
// If this is not an extra config request, we should check that the datasource exists and is of the correct type.
if isExtra, _ := f.isExtraConfig(ctx); !isExtra {
if isExtra := f.isExtraConfig(ctx); !isExtra {
_, err := getDatasourceByUID(ctx, f.DatasourceCache, apimodels.AlertmanagerBackend)
if err != nil {
return nil, err
@@ -54,22 +56,18 @@ func (f *AlertmanagerApiHandler) getService(ctx *contextmodel.ReqContext) (*Lote
// isExtraConfig checks if the datasourceUID represents an extra config.
// Extra configs are the alertmanager configurations that were saved using the Prometheus conversion API.
func (f *AlertmanagerApiHandler) isExtraConfig(ctx *contextmodel.ReqContext) (bool, string) {
func (f *AlertmanagerApiHandler) isExtraConfig(ctx *contextmodel.ReqContext) bool {
// Only enabled if feature flag is on
if !f.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerUI) {
return false, ""
return false
}
datasourceUID := web.Params(ctx.Req)[":DatasourceUID"]
if strings.HasPrefix(datasourceUID, extraConfigPrefix) {
identifier := strings.TrimPrefix(datasourceUID, extraConfigPrefix)
return true, identifier
}
return false, ""
return datasourceUID == withExtraConfigUID
}
func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *contextmodel.ReqContext, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
status := apimodels.GettableStatus{
Cluster: &amv2.ClusterStatus{
Status: util.Pointer("ready"),
@@ -87,7 +85,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMStatus(ctx *contextmodel.ReqCon
}
func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *contextmodel.ReqContext, body apimodels.PostableSilence, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return response.Error(http.StatusForbidden, "Read-only configuration", nil)
}
@@ -100,7 +98,7 @@ func (f *AlertmanagerApiHandler) handleRouteCreateSilence(ctx *contextmodel.ReqC
}
func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return response.Error(http.StatusForbidden, "Read-only configuration", nil)
}
@@ -113,7 +111,7 @@ func (f *AlertmanagerApiHandler) handleRouteDeleteAlertingConfig(ctx *contextmod
}
func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *contextmodel.ReqContext, silenceID string, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return response.Error(http.StatusForbidden, "Read-only configuration", nil)
}
@@ -126,18 +124,14 @@ func (f *AlertmanagerApiHandler) handleRouteDeleteSilence(ctx *contextmodel.ReqC
}
func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *contextmodel.ReqContext, dsUID string) response.Response {
if isExtra, identifier := f.isExtraConfig(ctx); isExtra {
ctx.Req.Header.Set(configIdentifierHeader, identifier)
ctx.Req.Header.Set("Accept", "application/yaml")
conversionResp := f.ConvertSvc.RouteConvertPrometheusGetAlertmanagerConfig(ctx)
if conversionResp.Status() != http.StatusOK {
return conversionResp
}
config, err := yamlExtractor(&apimodels.GettableUserConfig{})(conversionResp.(*response.NormalResponse))
if f.isExtraConfig(ctx) {
canSeeAutogen := ctx.HasRole(org.RoleAdmin)
config, err := f.GrafanaSvc.mam.GetAlertmanagerConfiguration(ctx.Req.Context(), ctx.GetOrgID(), canSeeAutogen, true)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to parse alertmanager config", err)
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return response.Error(http.StatusNotFound, "No configuration found", err)
}
return response.Error(http.StatusInternalServerError, "Failed to get merged configuration", err)
}
return response.JSON(http.StatusOK, config)
@@ -152,7 +146,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAlertingConfig(ctx *contextmodel.
}
func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *contextmodel.ReqContext, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return f.GrafanaSvc.RouteGetAMAlertGroups(ctx)
}
@@ -165,7 +159,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMAlertGroups(ctx *contextmodel.R
}
func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *contextmodel.ReqContext, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return f.GrafanaSvc.RouteGetAMAlerts(ctx)
}
@@ -178,7 +172,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetAMAlerts(ctx *contextmodel.ReqCon
}
func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *contextmodel.ReqContext, silenceID string, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return f.GrafanaSvc.RouteGetSilence(ctx, silenceID)
}
@@ -191,7 +185,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetSilence(ctx *contextmodel.ReqCont
}
func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *contextmodel.ReqContext, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return f.GrafanaSvc.RouteGetSilences(ctx)
}
@@ -204,7 +198,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetSilences(ctx *contextmodel.ReqCon
}
func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *contextmodel.ReqContext, body apimodels.PostableUserConfig, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return response.Error(http.StatusForbidden, "Read-only configuration", nil)
}
@@ -219,7 +213,7 @@ func (f *AlertmanagerApiHandler) handleRoutePostAlertingConfig(ctx *contextmodel
}
func (f *AlertmanagerApiHandler) handleRoutePostAMAlerts(ctx *contextmodel.ReqContext, body apimodels.PostableAlerts, dsUID string) response.Response {
if isExtra, _ := f.isExtraConfig(ctx); isExtra {
if f.isExtraConfig(ctx) {
return response.Error(http.StatusForbidden, "Read-only configuration", nil)
}
@@ -5,10 +5,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -18,39 +16,34 @@ import (
func TestAlertmanagerApiHandler_isExtraConfig(t *testing.T) {
tests := []struct {
name string
datasourceUID string
flagEnabled bool
expectedIsExtra bool
expectedIdentifier string
name string
datasourceUID string
flagEnabled bool
expectedIsExtra bool
}{
{
name: "normal datasource when feature enabled",
datasourceUID: "normal-datasource",
flagEnabled: true,
expectedIsExtra: false,
expectedIdentifier: "",
name: "normal datasource when feature enabled",
datasourceUID: "normal-datasource",
flagEnabled: true,
expectedIsExtra: false,
},
{
name: "extra config when feature enabled",
datasourceUID: "~grafana-converted-extra-config-test-config",
flagEnabled: true,
expectedIsExtra: true,
expectedIdentifier: "test-config",
name: "extra config when feature enabled",
datasourceUID: "~grafana-with-extra-config",
flagEnabled: true,
expectedIsExtra: true,
},
{
name: "extra config when feature disabled",
datasourceUID: "~grafana-converted-extra-config-test-config",
flagEnabled: false,
expectedIsExtra: false,
expectedIdentifier: "",
name: "extra config when feature disabled",
datasourceUID: "~grafana-with-extra-config",
flagEnabled: false,
expectedIsExtra: false,
},
{
name: "empty datasource UID",
datasourceUID: "",
flagEnabled: true,
expectedIsExtra: false,
expectedIdentifier: "",
name: "empty datasource UID",
datasourceUID: "",
flagEnabled: true,
expectedIsExtra: false,
},
}
@@ -81,9 +74,8 @@ func TestAlertmanagerApiHandler_isExtraConfig(t *testing.T) {
FeatureManager: features,
}
isExtra, identifier := handler.isExtraConfig(ctx)
isExtra := handler.isExtraConfig(ctx)
assert.Equal(t, tt.expectedIsExtra, isExtra)
assert.Equal(t, tt.expectedIdentifier, identifier)
})
}
}
@@ -101,14 +93,14 @@ func TestAlertmanagerApiHandler_ExtraConfigRouting(t *testing.T) {
}
ctx.Req = web.SetURLParams(req, map[string]string{
":DatasourceUID": "~grafana-converted-extra-config-test",
":DatasourceUID": "~grafana-with-extra-config",
})
handler := &AlertmanagerApiHandler{
FeatureManager: featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerUI),
}
resp := handler.handleRouteGetAMStatus(ctx, "~grafana-converted-extra-config-test")
resp := handler.handleRouteGetAMStatus(ctx, "~grafana-with-extra-config")
require.Equal(t, http.StatusOK, resp.Status())
})
@@ -124,80 +116,23 @@ func TestAlertmanagerApiHandler_ExtraConfigRouting(t *testing.T) {
}
ctx.Req = web.SetURLParams(req, map[string]string{
":DatasourceUID": "~grafana-converted-extra-config-test",
":DatasourceUID": "~grafana-with-extra-config",
})
handler := &AlertmanagerApiHandler{
FeatureManager: featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerUI),
}
resp := handler.handleRouteCreateSilence(ctx, apimodels.PostableSilence{}, "~grafana-converted-extra-config-test")
resp := handler.handleRouteCreateSilence(ctx, apimodels.PostableSilence{}, "~grafana-with-extra-config")
assert.Equal(t, http.StatusForbidden, resp.Status())
resp = handler.handleRouteDeleteAlertingConfig(ctx, "~grafana-converted-extra-config-test")
resp = handler.handleRouteDeleteAlertingConfig(ctx, "~grafana-with-extra-config")
assert.Equal(t, http.StatusForbidden, resp.Status())
resp = handler.handleRoutePostAlertingConfig(ctx, apimodels.PostableUserConfig{}, "~grafana-converted-extra-config-test")
resp = handler.handleRoutePostAlertingConfig(ctx, apimodels.PostableUserConfig{}, "~grafana-with-extra-config")
assert.Equal(t, http.StatusForbidden, resp.Status())
resp = handler.handleRoutePostAMAlerts(ctx, apimodels.PostableAlerts{}, "~grafana-converted-extra-config-test")
resp = handler.handleRoutePostAMAlerts(ctx, apimodels.PostableAlerts{}, "~grafana-with-extra-config")
assert.Equal(t, http.StatusForbidden, resp.Status())
})
t.Run("GET extra config", func(t *testing.T) {
req := &http.Request{
Header: make(http.Header),
}
ctx := &contextmodel.ReqContext{
Context: &web.Context{
Req: req,
},
SignedInUser: &user.SignedInUser{
OrgID: 1,
},
}
ctx.Req = web.SetURLParams(req, map[string]string{
":DatasourceUID": "~grafana-converted-extra-config-test-identifier",
})
mockConvertSvc := &mockConvertService{}
yamlConfig := `alertmanager_config: |
global: {}
route:
receiver: test-receiver
receivers:
- name: test-receiver`
mockResponse := response.Respond(http.StatusOK, yamlConfig).
SetHeader("Content-Type", "application/yaml")
mockConvertSvc.On("RouteConvertPrometheusGetAlertmanagerConfig", mock.Anything).
Run(func(args mock.Arguments) {
passedCtx := args.Get(0).(*contextmodel.ReqContext)
assert.Equal(t, "test-identifier", passedCtx.Req.Header.Get(configIdentifierHeader))
assert.Equal(t, "application/yaml", passedCtx.Req.Header.Get("Accept"))
}).
Return(mockResponse)
handler := &AlertmanagerApiHandler{
FeatureManager: featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerUI),
ConvertSvc: mockConvertSvc,
}
resp := handler.handleRouteGetAlertingConfig(ctx, "~grafana-converted-extra-config-test-identifier")
assert.Equal(t, http.StatusOK, resp.Status())
mockConvertSvc.AssertExpectations(t)
})
}
type mockConvertService struct {
mock.Mock
}
func (m *mockConvertService) RouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
args := m.Called(ctx)
return args.Get(0).(response.Response)
}
@@ -116,13 +116,14 @@ func (moa *MultiOrgAlertmanager) ApplyConfig(ctx context.Context, orgId int64, d
// GetAlertmanagerConfiguration returns the latest alertmanager configuration for a given org.
// If withAutogen is true, the configuration will be augmented with autogenerated routes.
func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool) (definitions.GettableUserConfig, error) {
// If withMergedExtraConfig is true, any extra configurations will be merged into the main configuration.
func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool, withMergedExtraConfig bool) (definitions.GettableUserConfig, error) {
amConfig, err := moa.configStore.GetLatestAlertmanagerConfiguration(ctx, org)
if err != nil {
return definitions.GettableUserConfig{}, fmt.Errorf("failed to get latest configuration: %w", err)
}
cfg, err := moa.gettableUserConfigFromAMConfigString(ctx, org, amConfig.AlertmanagerConfiguration)
cfg, err := moa.gettableUserConfigFromAMConfigString(ctx, org, amConfig.AlertmanagerConfiguration, withMergedExtraConfig)
if err != nil {
return definitions.GettableUserConfig{}, err
}
@@ -197,7 +198,7 @@ func (moa *MultiOrgAlertmanager) GetAppliedAlertmanagerConfigurations(ctx contex
gettableHistoricConfigs := make([]*definitions.GettableHistoricUserConfig, 0, len(configs))
for _, config := range configs {
appliedAt := strfmt.DateTime(time.Unix(config.LastApplied, 0).UTC())
gettableConfig, err := moa.gettableUserConfigFromAMConfigString(ctx, org, config.AlertmanagerConfiguration)
gettableConfig, err := moa.gettableUserConfigFromAMConfigString(ctx, org, config.AlertmanagerConfiguration, false)
if err != nil {
// If there are invalid records, skip them and return the valid ones.
moa.logger.Warn("Invalid configuration found in alert configuration history table", "id", config.ID, "orgID", org)
@@ -217,7 +218,7 @@ func (moa *MultiOrgAlertmanager) GetAppliedAlertmanagerConfigurations(ctx contex
return gettableHistoricConfigs, nil
}
func (moa *MultiOrgAlertmanager) gettableUserConfigFromAMConfigString(ctx context.Context, orgID int64, config string) (definitions.GettableUserConfig, error) {
func (moa *MultiOrgAlertmanager) gettableUserConfigFromAMConfigString(ctx context.Context, orgID int64, config string, withMergedExtraConfig bool) (definitions.GettableUserConfig, error) {
cfg, err := Load([]byte(config))
if err != nil {
return definitions.GettableUserConfig{}, fmt.Errorf("failed to unmarshal alertmanager configuration: %w", err)
@@ -228,10 +229,29 @@ func (moa *MultiOrgAlertmanager) gettableUserConfigFromAMConfigString(ctx contex
return definitions.GettableUserConfig{}, fmt.Errorf("failed to decrypt external configurations: %w", err)
}
var alertmanagerConfig definitions.PostableApiAlertingConfig
var templateFiles map[string]string
if withMergedExtraConfig && len(cfg.ExtraConfigs) > 0 {
mergeResult, err := cfg.GetMergedAlertmanagerConfig()
if err != nil {
return definitions.GettableUserConfig{}, fmt.Errorf("failed to merge configuration: %w", err)
}
alertmanagerConfig = mergeResult.Config
mergedTemplates := cfg.GetMergedTemplateDefinitions()
templateFiles = make(map[string]string, len(mergedTemplates))
for _, t := range mergedTemplates {
templateFiles[t.Name] = t.Content
}
} else {
alertmanagerConfig = cfg.AlertmanagerConfig
templateFiles = cfg.TemplateFiles
}
result := definitions.GettableUserConfig{
TemplateFiles: cfg.TemplateFiles,
TemplateFiles: templateFiles,
AlertmanagerConfig: definitions.GettableApiAlertingConfig{
Config: cfg.AlertmanagerConfig.Config,
Config: alertmanagerConfig.Config,
},
ExtraConfigs: cfg.ExtraConfigs,
}
@@ -239,13 +259,13 @@ func (moa *MultiOrgAlertmanager) gettableUserConfigFromAMConfigString(ctx contex
// First we encrypt the secure settings.
// This is done to ensure that any secure settings incorrectly stored in Settings are encrypted and moved to
// SecureSettings. This can happen if an integration definition is updated to make a field secure.
if err := EncryptReceiverConfigSettings(cfg.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
if err := EncryptReceiverConfigSettings(alertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
return moa.Crypto.Encrypt(ctx, payload, secrets.WithoutScope())
}); err != nil {
return definitions.GettableUserConfig{}, fmt.Errorf("failed to encrypt receivers: %w", err)
}
for _, recv := range cfg.AlertmanagerConfig.Receivers {
for _, recv := range alertmanagerConfig.Receivers {
receivers := make([]*definitions.GettableGrafanaReceiver, 0, len(recv.GrafanaManagedReceivers))
for _, pr := range recv.GrafanaManagedReceivers {
secureFields := make(map[string]bool, len(pr.SecureSettings))
@@ -48,7 +48,7 @@ receivers:
err := mam.SaveAndApplyExtraConfiguration(ctx, orgID, extraConfig)
require.NoError(t, err)
gettableConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false)
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)
@@ -98,7 +98,7 @@ receivers:
require.NoError(t, err)
// Verify only one config exists with updated content
gettableConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false)
gettableConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false)
require.NoError(t, err)
require.Len(t, gettableConfig.ExtraConfigs, 1)
require.Equal(t, identifier, gettableConfig.ExtraConfigs[0].Identifier)
@@ -172,14 +172,14 @@ receivers:
err := mam.SaveAndApplyExtraConfiguration(ctx, orgID, extraConfig)
require.NoError(t, err)
gettableConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false)
gettableConfig, err := mam.GetAlertmanagerConfiguration(ctx, orgID, false, false)
require.NoError(t, err)
require.Len(t, gettableConfig.ExtraConfigs, 1)
err = mam.DeleteExtraConfiguration(ctx, orgID, identifier)
require.NoError(t, err)
gettableConfig, err = mam.GetAlertmanagerConfiguration(ctx, orgID, false)
gettableConfig, err = mam.GetAlertmanagerConfiguration(ctx, orgID, false, false)
require.NoError(t, err)
require.Len(t, gettableConfig.ExtraConfigs, 0)
})
@@ -0,0 +1,109 @@
package alerting
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func TestIntegrationAlertmanagerExtraConfigMerging(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
testinfra.SQLiteIntegrationTest(t)
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{
"alertingImportAlertmanagerAPI",
"alertingImportAlertmanagerUI",
},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
t.Run("retrieve merged configuration via extra config datasource", func(t *testing.T) {
// first upload standard alertmanager configuration
baseConfig := apimodels.PostableUserConfig{}
baseConfigJSON := `{
"alertmanager_config": {
"route": {
"receiver": "base-default"
},
"receivers": [{
"name": "base-default",
"grafana_managed_receiver_configs": [{
"name": "base-email",
"type": "email",
"settings": {
"addresses": "example@grafana.com"
}
}]
}]
},
"template_files": {
"base.tmpl": "{{ define \"base.template\" }}Base Template{{ end }}"
}
}`
err := json.Unmarshal([]byte(baseConfigJSON), &baseConfig)
require.NoError(t, err)
err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, baseConfig)
require.NoError(t, err)
//now add extra configuration
extraHeaders := map[string]string{
"Content-Type": "application/yaml",
"X-Grafana-Alerting-Merge-Matchers": "imported=true",
}
extraConfigYAML := `
route:
receiver: extra-webhook
receivers:
- name: extra-webhook
webhook_configs:
- url: 'http://external.example.com/webhook'
`
extraConfig := apimodels.AlertmanagerUserConfig{
AlertmanagerConfig: extraConfigYAML,
TemplateFiles: map[string]string{
"extra.tmpl": `{{ define "extra.template" }}Extra Template{{ end }}`,
},
}
response := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, extraConfig, extraHeaders)
require.Equal(t, "success", response.Status)
// get the merged configuration and check it
datasourceUID := "~grafana-with-extra-config"
mergedConfig := apiClient.GetAlertmanagerConfigForDatasource(t, datasourceUID)
receiverNames := make(map[string]bool)
for _, recv := range mergedConfig.AlertmanagerConfig.Receivers {
receiverNames[recv.Name] = true
}
require.True(t, receiverNames["base-default"])
require.True(t, receiverNames["extra-webhook"])
require.Contains(t, mergedConfig.TemplateFiles, "base.tmpl", "Base template should be present")
require.Equal(t, `{{ define "base.template" }}Base Template{{ end }}`, mergedConfig.TemplateFiles["base.tmpl"])
require.Contains(t, mergedConfig.TemplateFiles, "extra.tmpl", "Extra template should be present")
require.Equal(t, `{{ define "extra.template" }}Extra Template{{ end }}`, mergedConfig.TemplateFiles["extra.tmpl"])
require.NotNil(t, mergedConfig.AlertmanagerConfig.Route)
})
}
+10
View File
@@ -1053,6 +1053,16 @@ func (a apiClient) GetAlertmanagerConfigWithStatus(t *testing.T) (apimodels.Gett
return sendRequestJSON[apimodels.GettableUserConfig](t, req, http.StatusOK)
}
func (a apiClient) GetAlertmanagerConfigForDatasource(t *testing.T, datasourceUID string) apimodels.GettableUserConfig {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/%s/config/api/v1/alerts", a.url, datasourceUID), nil)
require.NoError(t, err)
config, status, body := sendRequestJSON[apimodels.GettableUserConfig](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, body)
return config
}
func (a apiClient) GetActiveAlertsWithStatus(t *testing.T) (apimodels.AlertGroups, int, string) {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/api/v2/alerts/groups", a.url), nil)