mirror of
https://github.com/grafana/grafana.git
synced 2026-06-11 01:20:40 -04:00
* Alerting: Reject imported configs with unsupported receiver fields Imported Prometheus/Mimir Alertmanager configs that use receiver fields Grafana cannot represent (e.g. the *_file / *_ref fields removed in grafana/alerting#573) were silently dropped during conversion. Validate now round-trips each receiver through the upstream<->definition conversion and rejects any field that does not survive, listing the offending receivers and their YAML field paths in the error's "extra" payload. Detection is generic, so it stays correct as upstream adds fields. * Alerting: Add integration test for unsupported receiver fields import Asserts POST /convert/api/v1/alerts rejects a config whose receiver uses a field Grafana cannot represent (auth_password_file), returns the YAML field path, and stores nothing. * improve test
452 lines
16 KiB
Go
452 lines
16 KiB
Go
package alerting
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"path"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/grafana/alerting/receivers/opsgenie"
|
|
opsgeniev1 "github.com/grafana/alerting/receivers/opsgenie/v1"
|
|
"github.com/prometheus/alertmanager/config"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.yaml.in/yaml/v3"
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
|
"github.com/grafana/grafana/pkg/util/testutil"
|
|
)
|
|
|
|
func TestIntegrationConvertPrometheusAlertmanagerEndpoints(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
// Setup Grafana with alerting import feature flag enabled
|
|
|
|
testinfra.SQLiteIntegrationTest(t)
|
|
|
|
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
DisableLegacyAlerting: true,
|
|
EnableUnifiedAlerting: true,
|
|
DisableAnonymous: true,
|
|
AppModeProduction: true,
|
|
EnableFeatureToggles: []string{
|
|
featuremgmt.FlagAlertingMultiplePolicies,
|
|
featuremgmt.FlagAlertingImportAlertmanagerAPI,
|
|
},
|
|
})
|
|
|
|
grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, gpath)
|
|
|
|
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
|
|
|
configYaml, err := testData.ReadFile(path.Join("test-data", "mimir-alertmanager-post.yaml"))
|
|
require.NoError(t, err)
|
|
template, err := testData.ReadFile(path.Join("test-data", "mimir-alertmanager.tmpl"))
|
|
require.NoError(t, err)
|
|
expected, err := testData.ReadFile(path.Join("test-data", "mimir-alertmanager-get.yaml"))
|
|
require.NoError(t, err)
|
|
var expectedConfig map[string]any
|
|
require.NoError(t, yaml.Unmarshal(expected, &expectedConfig))
|
|
|
|
cleanup := func(identifier string) {
|
|
deleteHeaders := map[string]string{
|
|
"X-Grafana-Alerting-Config-Identifier": identifier,
|
|
}
|
|
_, status, _ := apiClient.RawConvertPrometheusDeleteAlertmanagerConfig(t, deleteHeaders)
|
|
require.Equal(t, http.StatusAccepted, status)
|
|
}
|
|
|
|
apiClient.EnsureMuteTiming(t, apimodels.MuteTimeInterval{MuteTimeInterval: config.MuteTimeInterval{Name: "maintenance_window"}})
|
|
apiClient.EnsureReceiver(t, apimodels.EmbeddedContactPoint{Name: "opsgenie", Type: string(opsgenie.Type), Settings: simplejson.MustJson([]byte(opsgeniev1.FullValidConfigForTesting))})
|
|
|
|
t.Run("create and get alertmanager configuration", func(t *testing.T) {
|
|
identifier := "test-create-get-config"
|
|
defer cleanup(identifier)
|
|
|
|
headers := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
"X-Grafana-Alerting-Config-Identifier": identifier,
|
|
}
|
|
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
TemplateFiles: map[string]string{
|
|
"mimir-alertmanager.tmpl": string(template),
|
|
},
|
|
}
|
|
|
|
response := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
|
require.Equal(t, "success", response.Status)
|
|
|
|
t.Run("should return renamed resources", func(t *testing.T) {
|
|
assert.Contains(t, response.RenameResources.Receivers, "opsgenie")
|
|
assert.Contains(t, response.RenameResources.TimeIntervals, "maintenance_window")
|
|
})
|
|
|
|
getHeaders := map[string]string{
|
|
"X-Grafana-Alerting-Config-Identifier": identifier,
|
|
}
|
|
retrievedConfig := apiClient.ConvertPrometheusGetAlertmanagerConfig(t, getHeaders)
|
|
var actualConfig map[string]any
|
|
require.NoError(t, yaml.Unmarshal([]byte(retrievedConfig.AlertmanagerConfig), &actualConfig))
|
|
|
|
diff := cmp.Diff(expectedConfig, actualConfig)
|
|
if diff != "" {
|
|
t.Fatalf("unexpected config (-want +got):\n%s", diff)
|
|
}
|
|
require.Contains(t, retrievedConfig.TemplateFiles, "mimir-alertmanager.tmpl")
|
|
require.Equal(t, string(template), retrievedConfig.TemplateFiles["mimir-alertmanager.tmpl"])
|
|
})
|
|
|
|
t.Run("delete alertmanager configuration", func(t *testing.T) {
|
|
identifier := "test-delete-config"
|
|
defer cleanup(identifier)
|
|
|
|
headers := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
"X-Grafana-Alerting-Config-Identifier": identifier,
|
|
}
|
|
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
TemplateFiles: map[string]string{
|
|
"mimir-alertmanager.tmpl": string(template),
|
|
},
|
|
}
|
|
|
|
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 use default identifier", func(t *testing.T) {
|
|
defer cleanup("")
|
|
|
|
headers := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
}
|
|
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
}
|
|
|
|
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
|
requireStatusCode(t, http.StatusAccepted, status, "")
|
|
|
|
getHeaders := map[string]string{
|
|
"X-Grafana-Alerting-Config-Identifier": "imported",
|
|
}
|
|
responseConfig, status, _ := apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, getHeaders)
|
|
requireStatusCode(t, http.StatusOK, status, "")
|
|
require.NotEmpty(t, responseConfig.AlertmanagerConfig)
|
|
})
|
|
|
|
t.Run("POST with invalid identifier should fail", func(t *testing.T) {
|
|
headers := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
"X-Grafana-Alerting-Config-Identifier": "-test-",
|
|
}
|
|
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
}
|
|
|
|
_, 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",
|
|
}
|
|
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: `invalid yaml: [[[`,
|
|
}
|
|
|
|
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
|
requireStatusCode(t, http.StatusBadRequest, status, "")
|
|
})
|
|
|
|
t.Run("POST with unsupported receiver fields should fail", func(t *testing.T) {
|
|
headers := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
"X-Grafana-Alerting-Config-Identifier": "test-unsupported-fields",
|
|
}
|
|
|
|
// auth_password_file references the filesystem, which Grafana's
|
|
// Alertmanager cannot represent; the import must be rejected rather
|
|
// than silently dropping it.
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: `
|
|
global:
|
|
smtp_smarthost: 'localhost:25'
|
|
smtp_from: 'alerts@example.com'
|
|
route:
|
|
receiver: a
|
|
receivers:
|
|
- name: a
|
|
email_configs:
|
|
- to: someone@example.com
|
|
auth_password_file: /etc/smtp-password
|
|
`,
|
|
}
|
|
|
|
_, status, body := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
|
requireStatusCode(t, http.StatusBadRequest, status, body)
|
|
require.Contains(t, body, "alerting.unsupportedReceiverFields")
|
|
require.Contains(t, body, "email_configs[0].auth_password_file")
|
|
|
|
// The rejected config must not have been stored.
|
|
_, status, _ = apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, map[string]string{
|
|
"X-Grafana-Alerting-Config-Identifier": "test-unsupported-fields",
|
|
})
|
|
requireStatusCode(t, http.StatusNotFound, status, "")
|
|
})
|
|
|
|
t.Run("DELETE without config identifier header should use default identifier", func(t *testing.T) {
|
|
createHeaders := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
}
|
|
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
}
|
|
|
|
_, status, _ := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, createHeaders)
|
|
requireStatusCode(t, http.StatusAccepted, status, "")
|
|
|
|
_, status, _ = apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, nil)
|
|
requireStatusCode(t, http.StatusOK, status, "")
|
|
|
|
_, status, _ = apiClient.RawConvertPrometheusDeleteAlertmanagerConfig(t, nil)
|
|
requireStatusCode(t, http.StatusAccepted, status, "")
|
|
|
|
_, status, _ = apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, nil)
|
|
requireStatusCode(t, http.StatusNotFound, status, "")
|
|
})
|
|
})
|
|
|
|
t.Run("update existing configuration", func(t *testing.T) {
|
|
identifier := "test-update-config"
|
|
defer cleanup(identifier)
|
|
|
|
headers := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
"X-Grafana-Alerting-Config-Identifier": identifier,
|
|
}
|
|
|
|
amConfig1 := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
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 := `
|
|
route:
|
|
group_by: ['service']
|
|
group_wait: 5s
|
|
group_interval: 5s
|
|
repeat_interval: 30m
|
|
receiver: updated-webhook
|
|
|
|
receivers:
|
|
- name: updated-webhook
|
|
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, "name: updated-webhook")
|
|
require.Contains(t, retrievedConfig.AlertmanagerConfig, "receiver: updated-webhook")
|
|
require.Contains(t, retrievedConfig.AlertmanagerConfig, "webhook_configs:")
|
|
|
|
require.Equal(t, `{{ define "updated.template" }}Updated Config{{ end }}`, retrievedConfig.TemplateFiles["updated.tmpl"])
|
|
})
|
|
|
|
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,
|
|
}
|
|
|
|
amConfig1 := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
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,
|
|
}
|
|
|
|
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)
|
|
|
|
t.Run("should override existing configuration if specified", func(t *testing.T) {
|
|
defer cleanup(secondIdentifier)
|
|
secondHeaders["X-Grafana-Alerting-Config-Force-Replace"] = "true"
|
|
|
|
response2 := apiClient.ConvertPrometheusPostAlertmanagerConfig(t, amConfig2, secondHeaders)
|
|
require.Equal(t, "success", response2.Status)
|
|
|
|
getHeaders := map[string]string{
|
|
"X-Grafana-Alerting-Config-Identifier": firstIdentifier,
|
|
}
|
|
|
|
_, status, _ := apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, getHeaders)
|
|
requireStatusCode(t, http.StatusNotFound, status, "")
|
|
|
|
getHeaders = map[string]string{
|
|
"X-Grafana-Alerting-Config-Identifier": secondIdentifier,
|
|
}
|
|
|
|
_, status, _ = apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, getHeaders)
|
|
requireStatusCode(t, http.StatusOK, status, "")
|
|
})
|
|
})
|
|
|
|
t.Run("dry-run should not create configuration", func(t *testing.T) {
|
|
identifier := "config"
|
|
// Create first configuration
|
|
firstHeaders := map[string]string{
|
|
"Content-Type": "application/yaml",
|
|
"X-Grafana-Alerting-Config-Identifier": identifier,
|
|
"X-Grafana-Alerting-Dry-Run": "true",
|
|
}
|
|
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
TemplateFiles: map[string]string{
|
|
"first.tmpl": `{{ define "first.template" }}First Config{{ end }}`,
|
|
},
|
|
}
|
|
|
|
_, status, body := apiClient.RawConvertPrometheusPostAlertmanagerConfig(t, amConfig, firstHeaders)
|
|
require.Equal(t, http.StatusOK, status)
|
|
|
|
response := apimodels.ConvertAlertmanagerResponse{}
|
|
err := json.Unmarshal([]byte(body), &response)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("should return renamed resources", func(t *testing.T) {
|
|
assert.Contains(t, response.RenameResources.Receivers, "opsgenie")
|
|
assert.Contains(t, response.RenameResources.TimeIntervals, "maintenance_window")
|
|
})
|
|
|
|
_, status, _ = apiClient.RawConvertPrometheusGetAlertmanagerConfig(t, firstHeaders)
|
|
requireStatusCode(t, http.StatusNotFound, status, "")
|
|
})
|
|
}
|
|
|
|
func TestIntegrationConvertPrometheusAlertmanagerEndpoints_FeatureFlagDisabled(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(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",
|
|
}
|
|
configYaml, err := testData.ReadFile(path.Join("test-data", "mimir-alertmanager-post.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
t.Run("POST should return not implemented when feature flag disabled", func(t *testing.T) {
|
|
amConfig := apimodels.AlertmanagerUserConfig{
|
|
AlertmanagerConfig: string(configYaml),
|
|
}
|
|
|
|
_, 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, "")
|
|
})
|
|
}
|