Alerting: Return merged extra configurations in the API (#109636)
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user