Alerting: API to create extra Alertmanager configurations (#106904)
What is this feature?
Implements the POST endpoint for deleting imported Mimir Alertmanager configurations:
POST /api/convert/api/v1/alerts
The API endpoint creates the extra Alertmanager configuration with the provided identifier and matchers.
This commit is contained in:
@@ -128,6 +128,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)
|
||||
}
|
||||
|
||||
@@ -532,7 +533,44 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, amCfg apimodels.AlertmanagerUserConfig) response.Response {
|
||||
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
|
||||
if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) {
|
||||
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
|
||||
}
|
||||
|
||||
logger := srv.logger.FromContext(c.Req.Context())
|
||||
|
||||
identifier, err := parseConfigIdentifierHeader(c)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse config identifier header", "error", err, "identifier", identifier)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
mergeMatchers, err := parseMergeMatchersHeader(c)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse merge matchers header", "error", err, "identifier", identifier)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
ec := apimodels.ExtraConfiguration{
|
||||
Identifier: identifier,
|
||||
MergeMatchers: mergeMatchers,
|
||||
TemplateFiles: amCfg.TemplateFiles,
|
||||
AlertmanagerConfig: amCfg.AlertmanagerConfig,
|
||||
}
|
||||
err = ec.Validate()
|
||||
if err != nil {
|
||||
logger.Error("Invalid alertmanager configuration", "error", err, "identifier", identifier)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
err = srv.am.SaveAndApplyExtraConfiguration(c.Req.Context(), c.GetOrgID(), ec)
|
||||
if err != nil {
|
||||
logger.Error("Failed to save alertmanager configuration", "error", err, "identifier", identifier)
|
||||
return errorToResponse(fmt.Errorf("failed to save alertmanager configuration: %w", err))
|
||||
}
|
||||
|
||||
logger.Info("Successfully updated alertmanager configuration with imported Prometheus config", "identifier", identifier)
|
||||
return successfulResponse()
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
|
||||
|
||||
@@ -1515,6 +1515,70 @@ func (m *mockAlertmanager) DeleteExtraConfiguration(ctx context.Context, org int
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestRouteConvertPrometheusPostAlertmanagerConfig(t *testing.T) {
|
||||
const identifier = "test-config"
|
||||
mockAM := &mockAlertmanager{}
|
||||
|
||||
ft := featuremgmt.WithFeatures(featuremgmt.FlagAlertingImportAlertmanagerAPI)
|
||||
srv, _, _ := createConvertPrometheusSrv(t, withAlertmanager(mockAM), withFeatureToggles(ft))
|
||||
|
||||
t.Run("should parse headers and call SaveAndApplyExtraConfiguration", func(t *testing.T) {
|
||||
mockAM.On("SaveAndApplyExtraConfiguration", mock.Anything, int64(1), mock.MatchedBy(func(extraConfig apimodels.ExtraConfiguration) bool {
|
||||
return extraConfig.Identifier == identifier &&
|
||||
len(extraConfig.MergeMatchers) == 2 &&
|
||||
len(extraConfig.TemplateFiles) == 1 &&
|
||||
extraConfig.TemplateFiles["test.tmpl"] == "{{ define \"test\" }}Hello{{ end }}"
|
||||
})).Return(nil).Once()
|
||||
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
||||
rc.Req.Header.Set(mergeMatchersHeader, "environment=production,team=backend")
|
||||
|
||||
amCfg := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: `{
|
||||
"route": {
|
||||
"receiver": "default"
|
||||
},
|
||||
"receivers": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
TemplateFiles: map[string]string{
|
||||
"test.tmpl": "{{ define \"test\" }}Hello{{ end }}",
|
||||
},
|
||||
}
|
||||
|
||||
response := srv.RouteConvertPrometheusPostAlertmanagerConfig(rc, amCfg)
|
||||
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
mockAM.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should return error when identifier header is missing", func(t *testing.T) {
|
||||
rc := createRequestCtx()
|
||||
|
||||
amCfg := apimodels.AlertmanagerUserConfig{}
|
||||
response := srv.RouteConvertPrometheusPostAlertmanagerConfig(rc, amCfg)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, response.Status())
|
||||
require.Contains(t, string(response.Body()), "identifier cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("should return error when merge matchers header has invalid format", func(t *testing.T) {
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(configIdentifierHeader, identifier)
|
||||
rc.Req.Header.Set(mergeMatchersHeader, "invalid-format")
|
||||
|
||||
amCfg := apimodels.AlertmanagerUserConfig{}
|
||||
response := srv.RouteConvertPrometheusPostAlertmanagerConfig(rc, amCfg)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, response.Status())
|
||||
require.Contains(t, string(response.Body()), "format should be 'key=value,key2=value2'")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteConvertPrometheusGetAlertmanagerConfig(t *testing.T) {
|
||||
const identifier = "test-config"
|
||||
const orgID = int64(1)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/grafana/alerting/definition"
|
||||
alertingmodels "github.com/grafana/alerting/models"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
)
|
||||
|
||||
// swagger:route POST /alertmanager/{DatasourceUID}/config/api/v1/alerts alertmanager RoutePostAlertingConfig
|
||||
@@ -272,8 +273,18 @@ type (
|
||||
const (
|
||||
GrafanaReceiverType = definition.GrafanaReceiverType
|
||||
AlertmanagerReceiverType = definition.AlertmanagerReceiverType
|
||||
|
||||
errInvalidExtraConfigurationMsg = "Invalid Alertmanager configuration: {{.Public.Error}}"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidExtraConfigurationBase = errutil.ValidationFailed("alerting.invalidExtraConfiguration").MustTemplate(errInvalidExtraConfigurationMsg, errutil.WithPublic(errInvalidExtraConfigurationMsg))
|
||||
)
|
||||
|
||||
func errInvalidExtraConfiguration(err error) error {
|
||||
return errInvalidExtraConfigurationBase.Build(errutil.TemplateData{Public: map[string]any{"Error": err}})
|
||||
}
|
||||
|
||||
var (
|
||||
AsGrafanaRoute = definition.AsGrafanaRoute
|
||||
AllReceivers = definition.AllReceivers
|
||||
@@ -673,12 +684,12 @@ func (c ExtraConfiguration) Validate() error {
|
||||
}
|
||||
|
||||
if len(c.MergeMatchers) == 0 {
|
||||
return errors.New("at least one matcher is required")
|
||||
return errInvalidExtraConfiguration(errors.New("at least one matcher is required"))
|
||||
}
|
||||
|
||||
for _, m := range c.MergeMatchers {
|
||||
if m.Type != labels.MatchEqual {
|
||||
return errors.New("only matchers with type equal are supported")
|
||||
return errInvalidExtraConfiguration(errors.New("only matchers with type equal are supported"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,7 +697,7 @@ func (c ExtraConfiguration) Validate() error {
|
||||
am := config.Config{}
|
||||
err := yaml.Unmarshal([]byte(c.AlertmanagerConfig), &am)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid alertmanager configuration: %w", err)
|
||||
return errInvalidExtraConfiguration(fmt.Errorf("failed to parse alertmanager config: %w", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -67,6 +68,75 @@ func TestAlertmanager_newAlertmanager(t *testing.T) {
|
||||
require.False(t, am.Ready())
|
||||
}
|
||||
|
||||
func TestAlertmanager_SaveAndApplyConfig_WithExternalSecrets(t *testing.T) {
|
||||
am := setupAMTest(t)
|
||||
|
||||
cfg := &definitions.PostableUserConfig{
|
||||
AlertmanagerConfig: definitions.PostableApiAlertingConfig{
|
||||
Config: definitions.Config{
|
||||
Route: &definitions.Route{
|
||||
Receiver: "default-receiver",
|
||||
},
|
||||
},
|
||||
Receivers: []*definitions.PostableApiReceiver{
|
||||
{
|
||||
Receiver: config.Receiver{Name: "default-receiver"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ExtraConfigs: []definitions.ExtraConfiguration{
|
||||
{
|
||||
Identifier: "external-prometheus",
|
||||
MergeMatchers: []*labels.Matcher{{Type: labels.MatchEqual, Name: "cluster", Value: "prod"}},
|
||||
AlertmanagerConfig: `
|
||||
route:
|
||||
receiver: webhook-receiver
|
||||
receivers:
|
||||
- name: webhook-receiver
|
||||
webhook_configs:
|
||||
- url: 'https://webhook.example.com/alerts'
|
||||
http_config:
|
||||
basic_auth:
|
||||
username: 'admin'
|
||||
password: 'super-secret-password'
|
||||
- url: 'https://slack.com/webhook/ABC123'
|
||||
send_resolved: true
|
||||
- name: email-receiver
|
||||
email_configs:
|
||||
- to: 'alerts@example.com'
|
||||
from: 'grafana@example.com'
|
||||
smarthost: 'smtp.gmail.com:587'
|
||||
auth_username: 'grafana@example.com'
|
||||
auth_password: 'another-secret-password'`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := am.SaveAndApplyConfig(context.Background(), cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
savedConfig, err := am.Store.GetLatestAlertmanagerConfiguration(context.Background(), am.Base.TenantID())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify secrets are encrypted in stored config
|
||||
var savedUserConfig definitions.PostableUserConfig
|
||||
err = json.Unmarshal([]byte(savedConfig.AlertmanagerConfiguration), &savedUserConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, savedUserConfig.ExtraConfigs, 1)
|
||||
extraConfig := savedUserConfig.ExtraConfigs[0]
|
||||
|
||||
require.Equal(t, "external-prometheus", extraConfig.Identifier)
|
||||
require.NotContains(t, extraConfig.AlertmanagerConfig, "super-secret-password")
|
||||
require.NotContains(t, extraConfig.AlertmanagerConfig, "another-secret-password")
|
||||
require.NotContains(t, extraConfig.AlertmanagerConfig, "ABC123")
|
||||
|
||||
// Apply the saved configuration again and check that it is applied without errors
|
||||
err = am.ApplyConfig(context.Background(), savedConfig)
|
||||
require.NoError(t, err)
|
||||
require.True(t, am.Ready())
|
||||
}
|
||||
|
||||
func TestAlertmanager_ApplyConfig(t *testing.T) {
|
||||
basicConfig := func() definitions.PostableApiAlertingConfig {
|
||||
return definitions.PostableApiAlertingConfig{
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
)
|
||||
|
||||
const testAlertmanagerConfigYAML = `
|
||||
global:
|
||||
smtp_smarthost: localhost:587
|
||||
smtp_from: alertmanager@example.org
|
||||
|
||||
route:
|
||||
group_by: ['alertname']
|
||||
group_wait: 10s
|
||||
group_interval: 10s
|
||||
repeat_interval: 1h
|
||||
receiver: web.hook
|
||||
|
||||
receivers:
|
||||
- name: web.hook
|
||||
webhook_configs:
|
||||
- url: 'http://127.0.0.1:5001/'
|
||||
|
||||
inhibit_rules:
|
||||
- source_match:
|
||||
severity: 'critical'
|
||||
target_match:
|
||||
severity: 'warning'
|
||||
equal: ['alertname', 'dev', 'instance']
|
||||
`
|
||||
|
||||
func TestIntegrationConvertPrometheusAlertmanagerEndpoints(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
// Setup Grafana with alerting import feature flag enabled
|
||||
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{
|
||||
"alertingImportAlertmanagerAPI",
|
||||
},
|
||||
})
|
||||
|
||||
grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, gpath)
|
||||
|
||||
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
||||
|
||||
t.Run("create and get alertmanager configuration", func(t *testing.T) {
|
||||
identifier := "test-create-get-config"
|
||||
mergeMatchers := "environment=production,team=backend"
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
"X-Grafana-Alerting-Merge-Matchers": mergeMatchers,
|
||||
}
|
||||
|
||||
amConfig := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
TemplateFiles: map[string]string{
|
||||
"test.tmpl": `{{ define "test.template" }}Test template{{ end }}`,
|
||||
},
|
||||
}
|
||||
|
||||
response := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
require.Equal(t, "success", response.Status)
|
||||
|
||||
getHeaders := map[string]string{
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
}
|
||||
retrievedConfig := apiClient.ConvertPrometheusGetAlertmanagerConfig(t, getHeaders)
|
||||
require.NotEmpty(t, retrievedConfig.AlertmanagerConfig)
|
||||
require.Contains(t, retrievedConfig.TemplateFiles, "test.tmpl")
|
||||
require.Equal(t, `{{ define "test.template" }}Test template{{ end }}`, retrievedConfig.TemplateFiles["test.tmpl"])
|
||||
|
||||
// Verify the configuration contains expected content
|
||||
require.Contains(t, retrievedConfig.AlertmanagerConfig, "smtp_smarthost: localhost:587")
|
||||
require.Contains(t, retrievedConfig.AlertmanagerConfig, "web.hook")
|
||||
|
||||
// Cleanup
|
||||
deleteHeaders := map[string]string{
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
}
|
||||
apiClient.ConvertPrometheusDeleteAlertmanagerConfig(t, deleteHeaders)
|
||||
})
|
||||
|
||||
t.Run("delete alertmanager configuration", func(t *testing.T) {
|
||||
identifier := "test-delete-config"
|
||||
mergeMatchers := "environment=production,team=backend"
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
"X-Grafana-Alerting-Merge-Matchers": mergeMatchers,
|
||||
}
|
||||
|
||||
amConfig := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
TemplateFiles: map[string]string{
|
||||
"test.tmpl": `{{ define "test.template" }}Test template{{ end }}`,
|
||||
},
|
||||
}
|
||||
|
||||
response := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
require.Equal(t, "success", response.Status)
|
||||
|
||||
deleteHeaders := map[string]string{
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
}
|
||||
apiClient.ConvertPrometheusDeleteAlertmanagerConfig(t, deleteHeaders)
|
||||
|
||||
// Verify configuration is deleted by trying to get it again
|
||||
getHeaders := map[string]string{
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
}
|
||||
_, status, _ := apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, getHeaders)
|
||||
requireStatusCode(t, http.StatusNotFound, status, "")
|
||||
})
|
||||
|
||||
t.Run("error cases", func(t *testing.T) {
|
||||
t.Run("POST without config identifier header should fail", func(t *testing.T) {
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Merge-Matchers": "environment=test",
|
||||
}
|
||||
|
||||
amConfig := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
}
|
||||
|
||||
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
requireStatusCode(t, http.StatusBadRequest, status, "")
|
||||
})
|
||||
|
||||
t.Run("POST without merge matchers header should fail", func(t *testing.T) {
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": "test-config",
|
||||
}
|
||||
|
||||
amConfig := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
}
|
||||
|
||||
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
requireStatusCode(t, http.StatusBadRequest, status, "")
|
||||
})
|
||||
|
||||
t.Run("POST with invalid merge matchers format should fail", func(t *testing.T) {
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": "test-invalid-matchers",
|
||||
"X-Grafana-Alerting-Merge-Matchers": "invalid-no-equals-sign",
|
||||
}
|
||||
|
||||
amConfig := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
}
|
||||
|
||||
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
requireStatusCode(t, http.StatusBadRequest, status, "")
|
||||
})
|
||||
|
||||
t.Run("POST with invalid alertmanager configuration should fail", func(t *testing.T) {
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": "test-invalid-yaml",
|
||||
"X-Grafana-Alerting-Merge-Matchers": "environment=test",
|
||||
}
|
||||
|
||||
amConfig := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: `invalid yaml: [[[`,
|
||||
}
|
||||
|
||||
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
requireStatusCode(t, http.StatusBadRequest, status, "")
|
||||
})
|
||||
|
||||
t.Run("DELETE without config identifier header should fail", func(t *testing.T) {
|
||||
headers := map[string]string{}
|
||||
|
||||
_, status, _ := apiClient.RawConvertPrometheusDeleteAlertmanagerConfig(t, headers)
|
||||
requireStatusCode(t, http.StatusBadRequest, status, "")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("update existing configuration", func(t *testing.T) {
|
||||
identifier := "test-update-config"
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
"X-Grafana-Alerting-Merge-Matchers": "environment=production",
|
||||
}
|
||||
|
||||
amConfig1 := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
TemplateFiles: map[string]string{
|
||||
"config1.tmpl": `{{ define "config1.template" }}Config 1{{ end }}`,
|
||||
},
|
||||
}
|
||||
|
||||
response1 := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, amConfig1, headers)
|
||||
require.Equal(t, "success", response1.Status)
|
||||
|
||||
// Update the same configuration with new content
|
||||
updatedConfigYAML := `
|
||||
global:
|
||||
smtp_smarthost: localhost:25
|
||||
smtp_from: updated@example.org
|
||||
|
||||
route:
|
||||
group_by: ['service']
|
||||
group_wait: 5s
|
||||
group_interval: 5s
|
||||
repeat_interval: 30m
|
||||
receiver: updated.hook
|
||||
|
||||
receivers:
|
||||
- name: updated.hook
|
||||
webhook_configs:
|
||||
- url: 'http://127.0.0.1:8080/updated'
|
||||
`
|
||||
|
||||
amConfig2 := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: updatedConfigYAML,
|
||||
TemplateFiles: map[string]string{
|
||||
"updated.tmpl": `{{ define "updated.template" }}Updated Config{{ end }}`,
|
||||
},
|
||||
}
|
||||
|
||||
response2 := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, amConfig2, headers)
|
||||
require.Equal(t, "success", response2.Status)
|
||||
|
||||
// Verify the updated configuration is retrieved
|
||||
getHeaders := map[string]string{
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
}
|
||||
retrievedConfig := apiClient.ConvertPrometheusGetAlertmanagerConfig(t, getHeaders)
|
||||
require.NotEmpty(t, retrievedConfig.AlertmanagerConfig)
|
||||
require.Contains(t, retrievedConfig.AlertmanagerConfig, "updated@example.org")
|
||||
require.Contains(t, retrievedConfig.AlertmanagerConfig, "updated.hook")
|
||||
require.Contains(t, retrievedConfig.TemplateFiles, "updated.tmpl")
|
||||
require.Equal(t, `{{ define "updated.template" }}Updated Config{{ end }}`, retrievedConfig.TemplateFiles["updated.tmpl"])
|
||||
|
||||
deleteHeaders := map[string]string{
|
||||
"X-Grafana-Alerting-Config-Identifier": identifier,
|
||||
}
|
||||
apiClient.ConvertPrometheusDeleteAlertmanagerConfig(t, deleteHeaders)
|
||||
})
|
||||
|
||||
t.Run("multiple extra configurations conflict", func(t *testing.T) {
|
||||
firstIdentifier := "first-config"
|
||||
secondIdentifier := "second-config"
|
||||
|
||||
// Create first configuration
|
||||
firstHeaders := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": firstIdentifier,
|
||||
"X-Grafana-Alerting-Merge-Matchers": "environment=first",
|
||||
}
|
||||
|
||||
amConfig1 := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
TemplateFiles: map[string]string{
|
||||
"first.tmpl": `{{ define "first.template" }}First Config{{ end }}`,
|
||||
},
|
||||
}
|
||||
|
||||
response1 := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, amConfig1, firstHeaders)
|
||||
require.Equal(t, "success", response1.Status)
|
||||
|
||||
// Try to create second configuration with different identifier,
|
||||
// it should fail because we don't support this yet.
|
||||
secondHeaders := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": secondIdentifier,
|
||||
"X-Grafana-Alerting-Merge-Matchers": "environment=second",
|
||||
}
|
||||
|
||||
amConfig2 := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: `
|
||||
global:
|
||||
smtp_smarthost: localhost:25
|
||||
|
||||
route:
|
||||
group_by: ['service']
|
||||
receiver: second.hook
|
||||
|
||||
receivers:
|
||||
- name: second.hook
|
||||
webhook_configs:
|
||||
- url: 'http://127.0.0.1:8080/second'
|
||||
`,
|
||||
TemplateFiles: map[string]string{
|
||||
"second.tmpl": `{{ define "second.template" }}Second Config{{ end }}`,
|
||||
},
|
||||
}
|
||||
|
||||
_, status, body := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig2, secondHeaders)
|
||||
requireStatusCode(t, http.StatusConflict, status, "")
|
||||
require.Contains(t, body, "multiple extra configurations are not supported")
|
||||
require.Contains(t, body, firstIdentifier)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationConvertPrometheusAlertmanagerEndpoints_FeatureFlagDisabled(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, gpath)
|
||||
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/yaml",
|
||||
"X-Grafana-Alerting-Config-Identifier": "test-config",
|
||||
"X-Grafana-Alerting-Merge-Matchers": "environment=test",
|
||||
}
|
||||
|
||||
t.Run("POST should return not implemented when feature flag disabled", func(t *testing.T) {
|
||||
amConfig := apimodels.AlertmanagerUserConfig{
|
||||
AlertmanagerConfig: testAlertmanagerConfigYAML,
|
||||
}
|
||||
|
||||
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
requireStatusCode(t, http.StatusNotImplemented, status, "")
|
||||
})
|
||||
|
||||
t.Run("GET should return not implemented when feature flag disabled", func(t *testing.T) {
|
||||
_, status, _ := apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, headers)
|
||||
requireStatusCode(t, http.StatusNotImplemented, status, "")
|
||||
})
|
||||
|
||||
t.Run("DELETE should return not implemented when feature flag disabled", func(t *testing.T) {
|
||||
_, status, _ := apiClient.RawConvertPrometheusDeleteAlertmanagerConfig(t, headers)
|
||||
requireStatusCode(t, http.StatusNotImplemented, status, "")
|
||||
})
|
||||
}
|
||||
@@ -1262,6 +1262,92 @@ func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle
|
||||
requireStatusCode(t, http.StatusAccepted, status, raw)
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusPostAlertmanagerConfig(t *testing.T, amCfg apimodels.AlertmanagerUserConfig, headers map[string]string) apimodels.ConvertPrometheusResponse {
|
||||
t.Helper()
|
||||
|
||||
resp, status, body := a.RawConvertPrometheusPostAlertmanagerConfig(t, amCfg, headers)
|
||||
requireStatusCode(t, http.StatusAccepted, status, body)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (a apiClient) RawConvertPrometheusPostAlertmanagerConfig(t *testing.T, amCfg apimodels.AlertmanagerUserConfig, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
|
||||
t.Helper()
|
||||
|
||||
path := "%s/api/convert/api/v1/alerts"
|
||||
|
||||
// Based on the content-type header, marshal the data to JSON or YAML
|
||||
contentType := headers["Content-Type"]
|
||||
var data []byte
|
||||
var err error
|
||||
if contentType == "application/json" {
|
||||
data, err = json.Marshal(amCfg)
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
data, err = yaml.Marshal(amCfg)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf(path, a.url), buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusGetAlertmanagerConfig(t *testing.T, headers map[string]string) apimodels.AlertmanagerUserConfig {
|
||||
t.Helper()
|
||||
|
||||
config, status, raw := a.RawConvertPrometheusGetAlertmanagerConfig(t, headers)
|
||||
requireStatusCode(t, http.StatusOK, status, raw)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (a apiClient) RawConvertPrometheusGetAlertmanagerConfig(t *testing.T, headers map[string]string) (apimodels.AlertmanagerUserConfig, int, string) {
|
||||
t.Helper()
|
||||
|
||||
path := "%s/api/convert/api/v1/alerts"
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
config, status, raw := sendRequestYAML[apimodels.AlertmanagerUserConfig](t, req, http.StatusOK)
|
||||
|
||||
return config, status, raw
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusDeleteAlertmanagerConfig(t *testing.T, headers map[string]string) {
|
||||
t.Helper()
|
||||
|
||||
_, status, raw := a.RawConvertPrometheusDeleteAlertmanagerConfig(t, headers)
|
||||
requireStatusCode(t, http.StatusAccepted, status, raw)
|
||||
}
|
||||
|
||||
func (a apiClient) RawConvertPrometheusDeleteAlertmanagerConfig(t *testing.T, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
|
||||
t.Helper()
|
||||
|
||||
path := "%s/api/convert/api/v1/alerts"
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (a apiClient) RawConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user