Alerting: Support for imported receivers in API (#112138)

* add support for converting Mimir integrations to Integration
* implement imported config revision
* update service to load staged receivers if configured
* make sure non-Grafana origin cannot be mutated
* set access control metadata for imported origin
* set includeImported from feature flag. Disabled for service used by provisioning
* add tests for new functionality
* add snapshot-based integration test
This commit is contained in:
Yuri Tseretyan
2025-11-13 15:35:21 +00:00
committed by GitHub
parent f041563df3
commit 6603abc873
15 changed files with 2054 additions and 84 deletions
@@ -2215,6 +2215,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
env.log,
ngalertfakes.NewFakeReceiverPermissionsService(),
tracer,
false,
)
return ProvisioningSrv{
log: env.log,
+12
View File
@@ -1256,6 +1256,18 @@ func (n ReceiverMutators) WithOrigin(origin ResourceOrigin) Mutator[Receiver] {
}
}
func (n ReceiverMutators) WithEmptyIntegrations() Mutator[Receiver] {
return func(r *Receiver) {
r.Integrations = []*Integration{}
}
}
func (n ReceiverMutators) WithUID(uid string) Mutator[Receiver] {
return func(r *Receiver) {
r.UID = uid
}
}
// Integrations
// CopyIntegrationWith creates a deep copy of Integration and then applies mutators to it.
+3
View File
@@ -408,6 +408,8 @@ func (ng *AlertNG) init() error {
ng.Log,
ng.ResourcePermissions,
ng.tracer,
//nolint:staticcheck // not yet migrated to OpenFeature
ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI),
)
provisioningReceiverService := notifier.NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](ng.accesscontrol, true),
@@ -419,6 +421,7 @@ func (ng *AlertNG) init() error {
ng.Log,
ng.ResourcePermissions,
ng.tracer,
false, // imported resources are not exposed via provisioning APIs
)
// Provisioning
@@ -71,6 +71,13 @@ func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, prov
if err != nil {
return nil, err
}
if postable.HasMimirIntegrations() {
mimir, err := PostableMimirReceiverToIntegrations(postable.Receiver)
if err != nil {
return nil, err
}
integrations = append(integrations, mimir...)
}
r := &models.Receiver{
UID: NameToUid(postable.GetName()), // TODO replace with stable UID.
Name: postable.GetName(),
@@ -117,6 +124,26 @@ func PostableGrafanaReceiversToIntegrations(postables []*apimodels.PostableGrafa
return integrations, nil
}
func PostableMimirReceiverToIntegrations(r alertingNotify.ConfigReceiver) ([]*models.Integration, error) {
v0, err := alertingNotify.ConfigReceiverToMimirIntegrations(r)
if err != nil {
return nil, fmt.Errorf("failed to convert v0 receiver to integrations: %w", err)
}
result := make([]*models.Integration, 0, len(v0))
for _, config := range v0 {
s, err := config.ConfigMap()
if err != nil {
return nil, fmt.Errorf("failed to get settings of v0 receiver %s (version %s): %w", config.Schema.Type(), config.Schema.Version, err)
}
result = append(result, &models.Integration{
Config: config.Schema,
Settings: s,
SecureSettings: map[string]string{},
})
}
return result, nil
}
func PostableGrafanaReceiverToIntegration(p *apimodels.PostableGrafanaReceiver) (*models.Integration, error) {
integrationType, err := alertingNotify.IntegrationTypeFromString(p.Type)
if err != nil {
@@ -0,0 +1,59 @@
package legacy_storage
import (
"fmt"
"reflect"
"testing"
"github.com/grafana/alerting/definition"
"github.com/grafana/alerting/notify"
"github.com/grafana/alerting/notify/notifytest"
"github.com/grafana/alerting/receivers/schema"
"github.com/grafana/alerting/receivers/teams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPostableMimirReceiverToIntegrations(t *testing.T) {
t.Run("can convert all known types", func(t *testing.T) {
notifytest.ForEachIntegrationTypeReceiver(t, func(configType reflect.Type, receiver notify.ConfigReceiver, rawConfig string) {
expectedType, err := notify.IntegrationTypeFromMimirTypeReflect(configType)
assert.NoError(t, err)
expectedVersion := schema.V0mimir1
if configType.Name() == "MSTeamsConfig" {
expectedType = teams.Type
}
if configType.Name() == "MSTeamsV2Config" {
expectedType = teams.Type
expectedVersion = schema.V0mimir2
}
t.Run(fmt.Sprintf("%s as %s %s", configType.Name(), expectedType, expectedVersion), func(t *testing.T) {
integrations, err := PostableMimirReceiverToIntegrations(receiver)
require.NoError(t, err)
require.Len(t, integrations, 1)
integration := integrations[0]
rawSettings, err := definition.MarshalJSONWithSecrets(integration.Settings)
require.NoError(t, err)
assert.EqualValues(t, expectedVersion, integration.Config.Version)
assert.EqualValues(t, expectedType, integration.Config.Type())
assert.JSONEq(t, rawConfig, string(rawSettings))
assert.Empty(t, integration.SecureSettings)
})
})
})
t.Run("can convert receiver with all integrations", func(t *testing.T) {
recv, err := notifytest.GetMimirReceiverWithAllIntegrations()
require.NoError(t, err)
integrations, err := PostableMimirReceiverToIntegrations(recv)
require.NoError(t, err)
require.Len(t, integrations, len(notifytest.AllValidMimirConfigs))
})
t.Run("returns empty if receiver has no integrations", func(t *testing.T) {
integrations, err := PostableMimirReceiverToIntegrations(notify.ConfigReceiver{Name: "test"})
require.NoError(t, err)
require.Empty(t, integrations)
})
}
@@ -0,0 +1,87 @@
package legacy_storage
import (
"fmt"
"slices"
"github.com/grafana/alerting/definition"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
type ImportedConfigRevision struct {
rev *ConfigRevision
opts definition.MergeOpts
importedConfig *definition.PostableApiAlertingConfig
}
func (rev *ConfigRevision) Imported() (ImportedConfigRevision, error) {
result := ImportedConfigRevision{
rev: rev,
}
if len(rev.Config.ExtraConfigs) == 0 {
return result, nil
}
// support only one config for now
mimirCfg := rev.Config.ExtraConfigs[0]
opts := definition.MergeOpts{
DedupSuffix: mimirCfg.Identifier,
SubtreeMatchers: mimirCfg.MergeMatchers,
}
if err := opts.Validate(); err != nil {
return result, fmt.Errorf("invalid merge options: %w", err)
}
mcfg, err := mimirCfg.GetAlertmanagerConfig()
if err != nil {
return result, fmt.Errorf("failed to get mimir alertmanager config: %w", err)
}
result.importedConfig = &mcfg
result.opts = opts
return result, nil
}
func (e ImportedConfigRevision) GetReceivers(uids []string) ([]*models.Receiver, error) {
if e.importedConfig == nil {
return nil, nil
}
original := e.rev.Config.AlertmanagerConfig.GetReceivers()
merged, _ := definition.MergeReceivers(original, e.importedConfig.GetReceivers(), e.opts.DedupSuffix)
capacity := len(uids)
if capacity == 0 {
capacity = len(e.importedConfig.Receivers)
}
result := make([]*models.Receiver, 0, capacity)
// merged config contains all receivers from both. We only want the ones from the staged config. However, we need to rename them if necessary.
for _, r := range merged[len(original):] {
uid := NameToUid(r.Name)
if len(uids) > 0 && !slices.Contains(uids, uid) {
continue
}
recv, err := PostableApiReceiverToReceiver(r, models.ProvenanceConvertedPrometheus, models.ResourceOriginImported)
if err != nil {
return nil, fmt.Errorf("failed to convert receiver %q: %w", r.Name, err)
}
result = append(result, recv)
}
return result, nil
}
// ReceiverUseByName returns a map of receiver names to the number of times they are used in routes.
func (e ImportedConfigRevision) ReceiverUseByName() map[string]int {
if e.importedConfig == nil {
return nil
}
m := make(map[string]int)
receiverUseCounts([]*definitions.Route{e.importedConfig.Route}, m)
_, renames := definition.MergeReceivers(e.rev.Config.AlertmanagerConfig.GetReceivers(), e.importedConfig.GetReceivers(), e.opts.DedupSuffix)
for original, renamed := range renames {
if cnt, ok := m[original]; ok {
delete(m, original)
m[renamed] = cnt
}
}
return m
}
@@ -0,0 +1,223 @@
package legacy_storage
import (
"testing"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
func TestConfigRevisionImported(t *testing.T) {
t.Run("should return error if extra config is invalid", func(t *testing.T) {
rev := getConfigRevisionForTest(withExtraConfig(extraConfig("invalid")))
_, err := rev.Imported()
require.Error(t, err)
})
t.Run("should return imported config if extra config is valid even if it cannot be fully merged", func(t *testing.T) {
rev := getConfigRevisionForTest(withExtraConfigAndConflictingMatchers())
imported, err := rev.Imported()
require.NoError(t, err)
require.NotNil(t, imported)
})
t.Run("should return imported config if extra config is valid", func(t *testing.T) {
rev := getConfigRevisionForTest(withExtraConfig(extraConfig(extraConfigurationYaml)))
imported, err := rev.Imported()
require.NoError(t, err)
require.NotNil(t, imported)
})
}
func TestConfigRevisionImported_GetReceivers(t *testing.T) {
rev := getConfigRevisionForTest(withExtraConfig(extraConfig(extraConfigurationYaml)))
imported, err := rev.Imported()
require.NoError(t, err)
t.Run("should return all receivers available in imported config", func(t *testing.T) {
receivers, err := imported.GetReceivers(nil)
require.NoError(t, err)
require.Len(t, receivers, 2)
assert.Equal(t, "imported-receiver-1", receivers[0].Name)
assert.Equal(t, NameToUid("imported-receiver-1"), receivers[0].UID)
assert.Equal(t, models.ProvenanceConvertedPrometheus, receivers[0].Provenance)
assert.Equal(t, models.ResourceOriginImported, receivers[0].Origin)
assert.Len(t, receivers[0].Integrations, 1)
assert.Equal(t, "imported-receiver-2", receivers[1].Name)
assert.Equal(t, NameToUid("imported-receiver-2"), receivers[1].UID)
assert.Equal(t, models.ProvenanceConvertedPrometheus, receivers[1].Provenance)
assert.Equal(t, models.ResourceOriginImported, receivers[1].Origin)
assert.Len(t, receivers[1].Integrations, 1)
})
t.Run("should filter receivers in imported config", func(t *testing.T) {
receivers, err := imported.GetReceivers([]string{models.NameToUid("imported-receiver-2")})
require.NoError(t, err)
require.Len(t, receivers, 1)
assert.Equal(t, "imported-receiver-2", receivers[0].Name)
assert.Equal(t, NameToUid("imported-receiver-2"), receivers[0].UID)
receivers, err = imported.GetReceivers([]string{models.NameToUid("not-found")})
require.NoError(t, err)
assert.Empty(t, receivers)
})
t.Run("should be correctly named in case of renames", func(t *testing.T) {
const dupeConfig = `
route:
receiver: receiver1
receivers:
- name: receiver1
- name: dupe-receiver
`
extra := extraConfig(dupeConfig)
expectedDedupSuffix := extra.Identifier
rev := getConfigRevisionForTest(withExtraConfig(extra))
imported, err = rev.Imported()
require.NoError(t, err)
result, err := imported.GetReceivers(nil)
require.NoError(t, err)
require.Len(t, result, 2)
assert.Equal(t, "receiver1"+expectedDedupSuffix, result[0].Name)
assert.Equal(t, models.NameToUid("receiver1"+expectedDedupSuffix), result[0].UID)
assert.Equal(t, "dupe-receiver"+expectedDedupSuffix, result[1].Name)
assert.Equal(t, models.NameToUid("dupe-receiver"+expectedDedupSuffix), result[1].UID)
t.Run("should search by renamed uid", func(t *testing.T) {
result, err = imported.GetReceivers([]string{NameToUid("receiver1")})
require.NoError(t, err)
require.Empty(t, result)
result, err = imported.GetReceivers([]string{NameToUid("receiver1" + expectedDedupSuffix)})
require.NoError(t, err)
require.Len(t, result, 1)
})
})
t.Run("should return empty list if no imported configuration", func(t *testing.T) {
rev := getConfigRevisionForTest()
imported, err = rev.Imported()
require.NoError(t, err)
assert.Nil(t, imported.importedConfig)
result, err := imported.GetReceivers(nil)
require.NoError(t, err)
require.Empty(t, result)
})
}
func TestConfigRevisionImported_ReceiverUseByName(t *testing.T) {
t.Run("should be empty if no configuration", func(t *testing.T) {
rev := getConfigRevisionForTest()
imported, err := rev.Imported()
require.NoError(t, err)
require.Empty(t, imported.ReceiverUseByName())
})
t.Run("should count usages of the receivers", func(t *testing.T) {
const cfg = `
route:
receiver: r1
routes:
- receiver: r2
routes:
- receiver: r1
- routes:
- receiver: r2
receivers:
- name: r1
- name: r2
`
extra := extraConfig(cfg)
rev := getConfigRevisionForTest(withExtraConfig(extra))
imported, err := rev.Imported()
require.NoError(t, err)
actual := imported.ReceiverUseByName()
require.EqualValues(t, map[string]int{
"": 1,
"r1": 2,
"r2": 2,
}, actual)
})
t.Run("should correctly handle deduplicated names", func(t *testing.T) {
const dupeConfig = `
route:
receiver: receiver1
routes:
- receiver: dupe-receiver
- receiver: r1
receivers:
- name: receiver1
- name: dupe-receiver
- name: r1
`
extra := extraConfig(dupeConfig)
expectedDedupSuffix := extra.Identifier
rev := getConfigRevisionForTest(withExtraConfig(extra))
imported, err := rev.Imported()
require.NoError(t, err)
actual := imported.ReceiverUseByName()
require.EqualValues(t, map[string]int{
"receiver1" + expectedDedupSuffix: 1,
"dupe-receiver" + expectedDedupSuffix: 1,
"r1": 1,
}, actual)
})
}
func extraConfig(yamlString string) definitions.ExtraConfiguration {
return definitions.ExtraConfiguration{
Identifier: "test",
MergeMatchers: config.Matchers{
&labels.Matcher{Type: labels.MatchEqual, Name: "__imported", Value: "test"},
},
AlertmanagerConfig: yamlString,
}
}
func withExtraConfig(extra definitions.ExtraConfiguration) opt {
return func(rev *ConfigRevision) {
rev.Config.ExtraConfigs = []definitions.ExtraConfiguration{
extra,
}
}
}
func withExtraConfigAndConflictingMatchers() opt {
return func(rev *ConfigRevision) {
matcher := &labels.Matcher{Type: labels.MatchEqual, Name: "__imported", Value: "test"}
rev.Config.AlertmanagerConfig.Route.Routes = append(rev.Config.AlertmanagerConfig.Route.Routes, &definitions.Route{
Matchers: []*labels.Matcher{matcher},
})
rev.Config.ExtraConfigs = []definitions.ExtraConfiguration{
{
Identifier: "test",
MergeMatchers: config.Matchers{
matcher,
},
TemplateFiles: nil,
AlertmanagerConfig: extraConfigurationYaml,
},
}
}
}
const extraConfigurationYaml = `
route:
receiver: imported-receiver-1
receivers:
- name: imported-receiver-1
webhook_configs:
- url: "http://localhost/"
- name: imported-receiver-2
webhook_configs:
- url: "http://localhost/"
`
@@ -469,8 +469,10 @@ func TestRenameReceiverInRoutes(t *testing.T) {
})
}
func getConfigRevisionForTest() *ConfigRevision {
return &ConfigRevision{
type opt func(*ConfigRevision)
func getConfigRevisionForTest(opts ...opt) *ConfigRevision {
r := &ConfigRevision{
Config: &definitions.PostableUserConfig{
AlertmanagerConfig: definitions.PostableApiAlertingConfig{
Config: definitions.Config{
@@ -519,4 +521,8 @@ func getConfigRevisionForTest() *ConfigRevision {
},
},
}
for _, o := range opts {
o(r)
}
return r
}
+121 -28
View File
@@ -55,6 +55,7 @@ type ReceiverService struct {
provenanceValidator validation.ProvenanceStatusTransitionValidator
resourcePermissions ac.ReceiverPermissionsService
tracer tracing.Tracer
includeImported bool
}
type alertRuleNotificationSettingsStore interface {
@@ -108,6 +109,7 @@ func NewReceiverService(
log log.Logger,
resourcePermissions ac.ReceiverPermissionsService,
tracer tracing.Tracer,
includeStaged bool,
) *ReceiverService {
return &ReceiverService{
authz: authz,
@@ -120,6 +122,7 @@ func NewReceiverService(
provenanceValidator: validation.ValidateProvenanceRelaxed,
resourcePermissions: resourcePermissions,
tracer: tracer,
includeImported: includeStaged,
}
}
@@ -149,7 +152,15 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver
rcv, err := revision.GetReceiver(legacy_storage.NameToUid(q.Name), prov)
if err != nil {
return nil, err
if errors.Is(err, legacy_storage.ErrReceiverNotFound) && rs.includeImported {
imported := rs.getImportedReceivers(ctx, span, []string{legacy_storage.NameToUid(q.Name)}, revision)
if len(imported) > 0 {
rcv = imported[0]
}
}
if rcv == nil {
return nil, err
}
}
span.AddEvent("Loaded receiver", trace.WithAttributes(
@@ -216,6 +227,11 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
attribute.Int("count", len(receivers)),
))
if rs.includeImported {
imported := rs.getImportedReceivers(ctx, span, uids, revision)
receivers = append(receivers, imported...)
}
filterFn := rs.authz.FilterReadDecrypted
if !q.Decrypt {
filterFn = rs.authz.FilterRead
@@ -270,14 +286,17 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle
existing, err := revision.GetReceiver(uid, prov)
if err != nil {
if errors.Is(err, legacy_storage.ErrReceiverNotFound) {
return nil
if !errors.Is(err, legacy_storage.ErrReceiverNotFound) {
return err
}
return err
}
if existing.Origin != models.ResourceOriginGrafana {
return makeErrReceiverOrigin(existing, "delete")
if rs.includeImported {
// try to get the imported receiver and return a specific error if it exists
result := rs.getImportedReceivers(ctx, span, []string{uid}, revision)
if len(result) > 0 {
return makeErrReceiverOrigin(result[0], "delete")
}
}
return nil
}
logger := rs.log.FromContext(ctx).New("receiver", existing.Name, "uid", uid, "version", version, "integrations", existing.GetIntegrationTypes())
@@ -395,6 +414,10 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
attribute.StringSlice("integrations", r.GetIntegrationTypes()),
))
defer span.End()
// check origin of the provided receiver
if r.Origin != models.ResourceOriginGrafana {
return nil, makeErrReceiverOrigin(r, "update")
}
if err := rs.authz.AuthorizeUpdate(ctx, user, r); err != nil {
return nil, err
@@ -415,6 +438,13 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
existing, err := revision.GetReceiver(r.GetUID(), prov)
if err != nil {
if errors.Is(err, legacy_storage.ErrReceiverNotFound) && rs.includeImported {
// try to get the imported receiver and return a specific error if it exists
result := rs.getImportedReceivers(ctx, span, []string{r.GetUID()}, revision)
if len(result) > 0 {
return nil, makeErrReceiverOrigin(result[0], "update")
}
}
return nil, err
}
@@ -440,10 +470,6 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
return nil, err
}
if existing.Origin != models.ResourceOriginGrafana {
return nil, makeErrReceiverOrigin(existing, "update")
}
if err := rs.provenanceValidator(existing.Provenance, r.Provenance); err != nil {
return nil, err
}
@@ -519,7 +545,21 @@ func (rs *ReceiverService) UsedByRules(ctx context.Context, orgID int64, name st
// AccessControlMetadata returns access control metadata for the given Receivers.
func (rs *ReceiverService) AccessControlMetadata(ctx context.Context, user identity.Requester, receivers ...*models.Receiver) (map[string]models.ReceiverPermissionSet, error) {
return rs.authz.Access(ctx, user, receivers...)
permissions, err := rs.authz.Access(ctx, user, receivers...)
if err != nil {
return nil, err
}
for _, m := range receivers {
if m.Origin == models.ResourceOriginGrafana {
continue
}
perms := permissions[m.GetUID()]
perms.Set(models.ReceiverPermissionAdmin, false)
perms.Set(models.ReceiverPermissionWrite, false)
perms.Set(models.ReceiverPermissionDelete, false)
permissions[m.GetUID()] = perms
}
return permissions, nil
}
// InUseMetadata returns metadata for the given Receivers about their usage in routes and rules.
@@ -528,31 +568,64 @@ func (rs *ReceiverService) InUseMetadata(ctx context.Context, orgID int64, recei
if err != nil {
return nil, err
}
receiverUses := revision.ReceiverUseByName()
q := models.ListNotificationSettingsQuery{OrgID: orgID}
if len(receivers) == 1 {
q.ReceiverName = receivers[0].Name
}
keys, err := rs.ruleNotificationsStore.ListNotificationSettings(ctx, q)
if err != nil {
return nil, err
var hasGrafanaOrigin, hasImportedOrigin bool
for i := range receivers {
switch receivers[i].Origin {
case models.ResourceOriginGrafana:
hasGrafanaOrigin = true
case models.ResourceOriginImported:
hasImportedOrigin = true
}
if hasGrafanaOrigin && hasImportedOrigin {
break
}
}
byReceiver := map[string][]models.AlertRuleKey{}
for key, settings := range keys {
for _, s := range settings {
if s.Receiver != "" {
byReceiver[s.Receiver] = append(byReceiver[s.Receiver], key)
var receiverUsesInRoutes map[string]int
var importedUsesInRoutes map[string]int
receiverUsesInRules := map[string][]models.AlertRuleKey{}
if hasGrafanaOrigin {
receiverUsesInRoutes = revision.ReceiverUseByName()
q := models.ListNotificationSettingsQuery{OrgID: orgID}
if len(receivers) == 1 {
q.ReceiverName = receivers[0].Name
}
keys, err := rs.ruleNotificationsStore.ListNotificationSettings(ctx, q)
if err != nil {
return nil, err
}
for key, settings := range keys {
for _, s := range settings {
if s.Receiver != "" {
receiverUsesInRules[s.Receiver] = append(receiverUsesInRules[s.Receiver], key)
}
}
}
}
if hasImportedOrigin {
s, err := revision.Imported()
if err == nil {
importedUsesInRoutes = s.ReceiverUseByName()
} else {
rs.log.FromContext(ctx).Warn("Unable to include imported receivers. Skipping", "err", err)
}
}
results := make(map[string]models.ReceiverMetadata, len(receivers))
for _, rcv := range receivers {
if rcv.Origin == models.ResourceOriginImported {
results[rcv.GetUID()] = models.ReceiverMetadata{
InUseByRoutes: importedUsesInRoutes[rcv.Name],
InUseByRules: nil,
CanUse: false,
}
continue
}
results[rcv.GetUID()] = models.ReceiverMetadata{
InUseByRoutes: receiverUses[rcv.Name],
InUseByRules: byReceiver[rcv.Name],
InUseByRoutes: receiverUsesInRoutes[rcv.Name],
InUseByRules: receiverUsesInRules[rcv.Name],
CanUse: rcv.Origin == models.ResourceOriginGrafana, // Only receivers from the Grafana configuration can be used.
}
}
@@ -741,3 +814,23 @@ func (rs *ReceiverService) RenameReceiverInDependentResources(ctx context.Contex
}
return nil
}
func (rs *ReceiverService) getImportedReceivers(ctx context.Context, span trace.Span, uids []string, revision *legacy_storage.ConfigRevision) []*models.Receiver {
var result []*models.Receiver
imported, err := revision.Imported()
if err == nil {
result, err = imported.GetReceivers(uids)
}
if err != nil {
rs.log.FromContext(ctx).Warn("Unable to include imported receivers. Skipping", "err", err)
span.RecordError(err, trace.WithAttributes(
attribute.String("concurrency_token", revision.ConcurrencyToken),
))
} else if len(result) > 0 { // if the list is empty, then we do not have any imported configuration
span.AddEvent("Loaded importedReceivers receivers", trace.WithAttributes(
attribute.String("concurrency_token", revision.ConcurrencyToken),
attribute.Int("count", len(result)),
))
}
return result
}
@@ -9,6 +9,7 @@ import (
"github.com/grafana/alerting/receivers/line"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -49,20 +50,43 @@ func TestIntegrationReceiverService_GetReceiver(t *testing.T) {
t.Run("service gets receiver from AM config", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
Receiver, err := sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
recv, err := sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
require.NoError(t, err)
require.Equal(t, "slack receiver", Receiver.Name)
require.Len(t, Receiver.Integrations, 1)
require.Equal(t, "UID2", Receiver.Integrations[0].UID)
require.Equal(t, "slack receiver", recv.Name)
require.Len(t, recv.Integrations, 1)
require.Equal(t, "UID2", recv.Integrations[0].UID)
})
t.Run("service returns error when receiver does not exist", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "nonexistent"), redactedUser)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "receiver1"), redactedUser)
require.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
})
t.Run("when includeImported is true", func(t *testing.T) {
t.Run("gets imported receivers", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService, withImportedIncluded)
recv, err := sut.GetReceiver(context.Background(), singleQ(1, "receiver1"), redactedUser)
require.NoError(t, err)
assert.Equal(t, models.ResourceOriginImported, recv.Origin)
assert.Equal(t, "receiver1", recv.Name)
assert.Equal(t, models.ProvenanceConvertedPrometheus, recv.Provenance)
require.Len(t, recv.Integrations, 2)
integration := recv.Integrations[0]
assert.Equal(t, "", integration.UID)
})
t.Run("falls to only Grafana if cannot read imported receivers", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService, withImportedIncluded, withInvalidExtraConfig)
_, err := sut.GetReceiver(context.Background(), singleQ(1, "receiver1"), redactedUser)
require.ErrorIs(t, err, legacy_storage.ErrReceiverNotFound)
_, err = sut.GetReceiver(context.Background(), singleQ(1, "slack receiver"), redactedUser)
require.NoError(t, err)
})
})
}
func TestIntegrationReceiverService_GetReceivers(t *testing.T) {
@@ -95,6 +119,29 @@ func TestIntegrationReceiverService_GetReceivers(t *testing.T) {
require.Len(t, Receivers, 1)
require.Equal(t, "slack receiver", Receivers[0].Name)
})
t.Run("when includeImported is true", func(t *testing.T) {
t.Run("returns imported receivers in the list", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService, withImportedIncluded)
recvs, err := sut.GetReceivers(context.Background(), multiQ(1), redactedUser)
require.NoError(t, err)
require.Len(t, recvs, 5)
names := make([]string, 0, len(recvs))
for _, recv := range recvs {
names = append(names, recv.Name)
}
expectedNames := []string{"grafana-default-email", "slack receiver", "receiver1", "empty receiver", "email-receiver"}
assert.ElementsMatch(t, expectedNames, names)
})
t.Run("falls to only Grafana if cannot read imported receivers", func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService, withImportedIncluded, withInvalidExtraConfig)
recvs, err := sut.GetReceivers(context.Background(), multiQ(1), redactedUser)
require.NoError(t, err)
require.Len(t, recvs, 2)
})
})
}
func TestIntegrationReceiverService_DecryptRedact(t *testing.T) {
@@ -153,51 +200,75 @@ func TestIntegrationReceiverService_DecryptRedact(t *testing.T) {
err: "",
},
} {
for _, method := range getMethods {
t.Run(fmt.Sprintf("%s %s", tc.name, method), func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService)
origin := []struct {
origin models.ResourceOrigin
receiver string
opts []createReceiverServiceSutOpt
secureSettingKey string
decryptedSettingValue string
}{
{
origin: models.ResourceOriginImported,
secureSettingKey: "auth_password",
decryptedSettingValue: "another-secret-password",
receiver: "email-receiver",
opts: []createReceiverServiceSutOpt{
withImportedIncluded,
},
},
{
origin: models.ResourceOriginGrafana,
secureSettingKey: "url",
decryptedSettingValue: "secure url",
receiver: "slack receiver",
},
}
var res *models.Receiver
var err error
if method == "single" {
q := singleQ(1, "slack receiver")
q.Decrypt = tc.decrypt
res, err = sut.GetReceiver(context.Background(), q, tc.user)
} else {
q := multiQ(1, "slack receiver")
q.Decrypt = tc.decrypt
var multiRes []*models.Receiver
multiRes, err = sut.GetReceivers(context.Background(), q, tc.user)
if tc.err == "" {
require.Len(t, multiRes, 1)
res = multiRes[0]
}
}
if tc.err == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.err)
}
for _, o := range origin {
for _, method := range getMethods {
t.Run(fmt.Sprintf("%s %s", tc.name, method), func(t *testing.T) {
t.Run(fmt.Sprintf("%s %s (%s)", tc.name, method, o.origin), func(t *testing.T) {
sut := createReceiverServiceSut(t, secretsService, o.opts...)
if tc.err == "" {
require.Equal(t, "slack receiver", res.Name)
require.Len(t, res.Integrations, 1)
require.Equal(t, "UID2", res.Integrations[0].UID)
var res *models.Receiver
var err error
if method == "single" {
q := singleQ(1, o.receiver)
q.Decrypt = tc.decrypt
res, err = sut.GetReceiver(context.Background(), q, tc.user)
} else {
q := multiQ(1, o.receiver)
q.Decrypt = tc.decrypt
var multiRes []*models.Receiver
multiRes, err = sut.GetReceivers(context.Background(), q, tc.user)
if tc.err == "" {
require.Len(t, multiRes, 1)
res = multiRes[0]
}
}
if tc.err != "" {
require.ErrorContains(t, err, tc.err)
return
}
require.NoError(t, err)
if tc.decrypt {
require.Equal(t, "secure url", res.Integrations[0].Settings["url"])
require.NotContains(t, res.Integrations[0].SecureSettings, "url")
} else {
require.NotContains(t, res.Integrations[0].Settings, "url")
require.Equal(t, o.receiver, res.Name)
require.Len(t, res.Integrations, 1)
require.NoError(t, err)
// Ensure the encrypted value exists and is not redacted or decrypted.
require.NotEmpty(t, res.Integrations[0].SecureSettings["url"])
require.NotEqual(t, definitions.RedactedValue, res.Integrations[0].SecureSettings["url"])
require.NotEqual(t, "secure url", res.Integrations[0].SecureSettings["url"])
}
}
})
if tc.decrypt {
require.Equal(t, o.decryptedSettingValue, res.Integrations[0].Settings[o.secureSettingKey])
require.NotContains(t, res.Integrations[0].SecureSettings, o.secureSettingKey)
} else {
require.NotContains(t, res.Integrations[0].Settings, o.secureSettingKey)
// Ensure the encrypted value exists and is not redacted or decrypted.
require.NotEmpty(t, res.Integrations[0].SecureSettings[o.secureSettingKey])
require.NotEqual(t, definitions.RedactedValue, res.Integrations[0].SecureSettings[o.secureSettingKey])
require.NotEqual(t, o.decryptedSettingValue, res.Integrations[0].SecureSettings[o.secureSettingKey])
}
})
})
}
}
}
}
@@ -225,6 +296,7 @@ func TestReceiverService_Delete(t *testing.T) {
storeSettings map[models.AlertRuleKey][]models.NotificationSettings
existing *models.Receiver
expectedErr error
opts []createReceiverServiceSutOpt
}{
{
name: "service deletes receiver",
@@ -293,13 +365,32 @@ func TestReceiverService_Delete(t *testing.T) {
version: "wrong version",
expectedErr: ErrReceiverVersionConflict,
},
{
name: "delete of receiver with non-Grafana origin fails",
user: writer,
deleteUID: legacy_storage.NameToUid("empty receiver"),
expectedErr: ErrReceiverOrigin,
opts: []createReceiverServiceSutOpt{
withImportedIncluded,
},
},
{
name: "delete of receiver succeeds even with invalid imported config",
user: writer,
deleteUID: baseReceiver.UID,
existing: util.Pointer(baseReceiver.Clone()),
opts: []createReceiverServiceSutOpt{
withInvalidExtraConfig,
withImportedIncluded,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
store := &fakeAlertRuleNotificationStore{}
store.ListNotificationSettingsFn = func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
return tc.storeSettings, nil
}
sut := createReceiverServiceSut(t, &secretsService)
sut := createReceiverServiceSut(t, &secretsService, tc.opts...)
sut.ruleNotificationsStore = store
if tc.existing != nil {
@@ -364,6 +455,7 @@ func TestReceiverService_Create(t *testing.T) {
expectedStored *definitions.PostableApiReceiver
expectedErr error
expectedProvenances map[string]models.Provenance
opts []createReceiverServiceSutOpt
}{
{
name: "service creates receiver",
@@ -482,9 +574,20 @@ func TestReceiverService_Create(t *testing.T) {
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithName("")),
expectedErr: legacy_storage.ErrReceiverInvalid,
},
{
name: "should be able to create receiver with the same name as imported ones",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithName("receiver1")),
expectedCreate: models.CopyReceiverWith(baseReceiver,
models.ReceiverMuts.Encrypted(models.Base64Enrypt),
models.ReceiverMuts.WithName("receiver1"),
),
expectedProvenances: map[string]models.Provenance{slackIntegration.UID: models.ProvenanceNone},
opts: []createReceiverServiceSutOpt{withImportedIncluded},
},
} {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
sut := createReceiverServiceSut(t, &secretsService, tc.opts...)
created, err := sut.CreateReceiver(context.Background(), &tc.receiver, tc.user.GetOrgID(), tc.user)
if tc.expectedErr == nil {
@@ -586,6 +689,7 @@ func TestReceiverService_Update(t *testing.T) {
expectedUpdate models.Receiver
expectedProvenances map[string]models.Provenance
expectedErr error
opts []createReceiverServiceSutOpt
}{
{
name: "copies existing secure fields",
@@ -741,9 +845,31 @@ func TestReceiverService_Update(t *testing.T) {
existing: util.Pointer(baseReceiver.Clone()),
expectedErr: legacy_storage.ErrReceiverInvalid,
},
{
name: "receivers with non-Grafana origin are not accepted",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithOrigin(models.ResourceOriginImported)),
existing: util.Pointer(baseReceiver.Clone()),
expectedErr: ErrReceiverOrigin,
},
{
name: "receivers of non-Grafana origin cannot be updated",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithName("receiver1")),
expectedErr: ErrReceiverOrigin,
opts: []createReceiverServiceSutOpt{withImportedIncluded},
},
{
name: "update should not fail if imported cannot be included",
user: writer,
receiver: models.CopyReceiverWith(baseReceiver, rm.WithEmptyIntegrations()),
existing: util.Pointer(models.CopyReceiverWith(baseReceiver)),
expectedUpdate: models.CopyReceiverWith(baseReceiver, rm.WithEmptyIntegrations()),
opts: []createReceiverServiceSutOpt{withImportedIncluded, withInvalidExtraConfig},
},
} {
t.Run(tc.name, func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
sut := createReceiverServiceSut(t, &secretsService, tc.opts...)
if tc.existing != nil {
// Create route after receivers as they will be referenced.
@@ -813,6 +939,9 @@ func TestReceiverService_Update(t *testing.T) {
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
require.NoError(t, err)
if tc.expectedProvenances == nil {
tc.expectedProvenances = make(map[string]models.Provenance)
}
assert.Equal(t, tc.expectedProvenances, provenances)
})
}
@@ -917,6 +1046,30 @@ func TestReceiverService_UpdateReceiverName(t *testing.T) {
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.ErrorIs(t, err, legacy_storage.ErrReceiverInvalid)
})
t.Run("can rename receiver to name that is already used by another receiver of different origin", func(t *testing.T) {
ruleStore := &fakeAlertRuleNotificationStore{}
sut := createReceiverServiceSut(t, &secretsService, withImportedIncluded)
sut.ruleNotificationsStore = ruleStore
newReceiverName = "receiver1"
actual, err := sut.GetReceiver(context.Background(), models.GetReceiverQuery{OrgID: writer.GetOrgID(), Name: newReceiverName}, writer)
require.NoError(t, err)
require.Equal(t, models.ResourceOriginImported, actual.Origin)
require.Equal(t, newReceiverName, actual.Name)
require.NotEmpty(t, actual.Integrations)
baseReceiver.Name = newReceiverName
recv, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.NoError(t, err)
require.NotEqual(t, actual, recv)
require.Equal(t, models.ResourceOriginGrafana, recv.Origin)
actual, err = sut.GetReceiver(context.Background(), models.GetReceiverQuery{OrgID: writer.GetOrgID(), Name: newReceiverName}, writer)
require.NoError(t, err)
require.Equal(t, recv.Name, actual.Name)
})
}
func TestReceiverServiceAC_Read(t *testing.T) {
@@ -1557,13 +1710,61 @@ func TestReceiverService_InUseMetadata(t *testing.T) {
}
}
func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverService {
cfg := createEncryptedConfig(t, encryptSvc)
func TestReceiverService_AccessControlMetadata(t *testing.T) {
secretsService := fake_secrets.NewFakeSecretsService()
sut := createReceiverServiceSut(t, &secretsService, withImportedIncluded)
admin := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleAdmin, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
accesscontrol.ActionAlertingReceiversReadSecrets: []string{
models.ScopeReceiversProvider.GetResourceAllScope(),
},
},
}}
r, err := sut.GetReceiver(context.Background(), models.GetReceiverQuery{OrgID: 1, Name: "receiver1"}, admin)
require.NoError(t, err)
t.Run("should override metadata for imported receivers", func(t *testing.T) {
meta, err := sut.AccessControlMetadata(context.Background(), admin, r)
require.NoError(t, err)
expectedPermissions := models.NewReceiverPermissionSet()
expectedPermissions.Set(models.ReceiverPermissionAdmin, false)
expectedPermissions.Set(models.ReceiverPermissionWrite, false)
expectedPermissions.Set(models.ReceiverPermissionDelete, false)
expectedPermissions.Set(models.ReceiverPermissionReadSecret, true)
expected := map[string]models.ReceiverPermissionSet{
r.GetUID(): expectedPermissions,
}
assert.Equal(t, expected, meta)
})
}
type createReceiverServiceSutOpt func(*testing.T, *ReceiverService)
func withInvalidExtraConfig(t *testing.T, sut *ReceiverService) {
t.Helper()
extra := getExtraConfig()
extra.AlertmanagerConfig = "yaml:invalid"
cfg := createEncryptedConfig(t, sut.encryptionService, extra)
store := fakes.NewFakeAlertmanagerConfigStore(cfg)
sut.cfgStore = legacy_storage.NewAlertmanagerConfigStore(store, NewExtraConfigsCrypto(sut.encryptionService))
}
func withImportedIncluded(_ *testing.T, sut *ReceiverService) {
sut.includeImported = true
}
func createReceiverServiceSut(t *testing.T, encryptSvc secretService, opts ...createReceiverServiceSutOpt) *ReceiverService {
cfg := createEncryptedConfig(t, encryptSvc, getExtraConfig())
store := fakes.NewFakeAlertmanagerConfigStore(cfg)
xact := newNopTransactionManager()
provisioningStore := fakes.NewFakeProvisioningStore()
return NewReceiverService(
sut := NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), false),
legacy_storage.NewAlertmanagerConfigStore(store, NewExtraConfigsCrypto(encryptSvc)),
provisioningStore,
@@ -1573,10 +1774,15 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverS
log.NewNopLogger(),
fakes.NewFakeReceiverPermissionsService(),
tracing.InitializeTracerForTest(),
false,
)
for _, opt := range opts {
opt(t, sut)
}
return sut
}
func createEncryptedConfig(t *testing.T, secretService secretService) string {
func createEncryptedConfig(t *testing.T, secretService secretService, extraConfig *definitions.ExtraConfiguration) string {
c := &definitions.PostableUserConfig{}
err := json.Unmarshal([]byte(defaultAlertmanagerConfigJSON), c)
require.NoError(t, err)
@@ -1584,6 +1790,10 @@ func createEncryptedConfig(t *testing.T, secretService secretService) string {
return secretService.Encrypt(ctx, payload, secrets.WithoutScope())
})
require.NoError(t, err)
if extraConfig != nil {
c.ExtraConfigs = append(c.ExtraConfigs, *extraConfig)
require.NoError(t, NewExtraConfigsCrypto(secretService).EncryptExtraConfigs(context.Background(), c))
}
bytes, err := json.Marshal(c)
require.NoError(t, err)
return string(bytes)
@@ -1645,6 +1855,40 @@ const defaultAlertmanagerConfigJSON = `
}
`
func getExtraConfig() *definitions.ExtraConfiguration {
return &definitions.ExtraConfiguration{
Identifier: "import",
MergeMatchers: []*labels.Matcher{{Type: labels.MatchEqual, Name: "__imported", Value: "true"}},
TemplateFiles: nil,
AlertmanagerConfig: defaultExtraConfig,
}
}
const defaultExtraConfig = `
route:
receiver: receiver1
routes:
- receiver: email-receiver
receivers:
- name: empty receiver
- name: receiver1
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'
`
type NopTransactionManager struct{}
func newNopTransactionManager() *NopTransactionManager {
@@ -570,6 +570,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec
log.NewNopLogger(),
fakes.NewFakeReceiverPermissionsService(),
tracing.InitializeTracerForTest(),
false,
)
return NewContactPointService(
@@ -332,6 +332,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
ps.log,
ps.resourcePermissions,
ps.tracer,
false,
)
contactPointService := provisioning.NewContactPointService(configStore, ps.secretService,
ps.alertingStore, ps.SQLStore, receiverSvc, ps.log, ps.alertingStore, ps.resourcePermissions)
@@ -0,0 +1,148 @@
package receivers
import (
"bytes"
"context"
"encoding/json"
"os"
"path"
"testing"
"github.com/grafana/alerting/notify"
"github.com/grafana/alerting/receivers/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
"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/models"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func TestIntegrationReadImported_Snapshot(t *testing.T) {
ctx := context.Background()
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{
featuremgmt.FlagAlertingImportAlertmanagerAPI,
},
})
receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
cliCfg := helper.Org1.Admin.NewRestConfig()
alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
configYaml, err := testData.ReadFile(path.Join("test-data", "imported.yaml"))
require.NoError(t, err)
identifier := "test-create-get-config"
mergeMatchers := "_imported=true"
headers := map[string]string{
"Content-Type": "application/yaml",
"X-Grafana-Alerting-Config-Identifier": identifier,
"X-Grafana-Alerting-Merge-Matchers": mergeMatchers,
}
amConfig := apimodels.AlertmanagerUserConfig{
AlertmanagerConfig: string(configYaml),
}
response := alertingApi.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
require.Equal(t, "success", response.Status)
receiversRaw, err := receiverClient.Client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
raw, err := receiversRaw.MarshalJSON()
require.NoError(t, err)
expectedBytes, err := os.ReadFile(path.Join("test-data", "imported-expected-snapshot.json"))
require.NoError(t, err)
if !assert.JSONEq(t, string(expectedBytes), string(raw)) {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, raw, "", " ")
require.NoError(t, err)
err = os.WriteFile(path.Join("test-data", "imported-expected-snapshot.json"), prettyJSON.Bytes(), 0o644)
require.NoError(t, err)
}
receivers, err := receiverClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
t.Run("secure fields should be properly masked", func(t *testing.T) {
for _, receiver := range receivers.Items {
if receiver.Spec.Title == "grafana-default-email" {
continue
}
for _, integration := range receiver.Spec.Integrations {
sch, ok := notify.GetSchemaVersionForIntegration(schema.IntegrationType(integration.Type), schema.Version(integration.Version))
require.Truef(t, ok, "unknown integration type %s and version %s", integration.Type, integration.Version)
keys := flattenKeys(integration.Settings)
secrets := make(map[string]struct{})
for _, fieldPath := range sch.GetSecretFieldsPaths() {
assert.NotContainsf(t, keys, fieldPath.String(), "receiver %s integration %s has secret field %s", receiver.Name, integration.Type, fieldPath.String())
secrets[fieldPath.String()] = struct{}{}
}
for key := range integration.SecureFields {
assert.Containsf(t, secrets, key, "receiver %s integration %s has secure field %s that is not in the schema", receiver.Name, integration.Type, key)
}
}
}
})
t.Run("should set the correct annotations", func(t *testing.T) {
for _, receiver := range receivers.Items {
if receiver.Spec.Title == "grafana-default-email" {
continue
}
assert.EqualValuesf(t, models.ProvenanceConvertedPrometheus, receiver.GetProvenanceStatus(), "receiver %s has unexpected provenance", receiver.Name)
assert.Equalf(t, "false", receiver.Annotations[v0alpha1.CanUseAnnotationKey], "receiver %s has unexpected can use annotation", receiver.Name)
assert.Equalf(t, "", receiver.Annotations[v0alpha1.AccessControlAnnotation("canAdmin")], "receiver %s has unexpected can admin annotation", receiver.Name)
assert.Equalf(t, "", receiver.Annotations[v0alpha1.AccessControlAnnotation("canDelete")], "receiver %s has unexpected can delete annotation", receiver.Name)
assert.Equalf(t, "true", receiver.Annotations[v0alpha1.AccessControlAnnotation("canReadSecrets")], "receiver %s has unexpected can read secrets annotation", receiver.Name)
assert.Equalf(t, "", receiver.Annotations[v0alpha1.AccessControlAnnotation("canWrite")], "receiver %s has unexpected can write annotation", receiver.Name)
}
})
t.Run("should not be able to update", func(t *testing.T) {
toUpdate := receivers.Items[1]
toUpdate.Spec.Title = "another title"
_, err = receiverClient.Update(ctx, &toUpdate, v1.UpdateOptions{})
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
})
t.Run("should not be able to delete", func(t *testing.T) {
toDelete := receivers.Items[1]
err = receiverClient.Delete(ctx, toDelete.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
})
}
func flattenKeys(m map[string]any) map[string]struct{} {
result := map[string]struct{}{}
flattenHelper("", m, result)
return result
}
func flattenHelper(prefix string, m map[string]any, result map[string]struct{}) {
for key, value := range m {
newKey := key
if prefix != "" {
newKey = prefix + "." + key
}
if valMap, ok := value.(map[string]any); ok {
flattenHelper(newKey, valMap, result)
} else {
result[newKey] = struct{}{}
}
}
}
@@ -0,0 +1,818 @@
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"items": [
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canAdmin": "true",
"grafana.com/access/canDelete": "true",
"grafana.com/access/canReadSecrets": "true",
"grafana.com/access/canWrite": "true",
"grafana.com/canUse": "true",
"grafana.com/inUse/routes": "1",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "none"
},
"name": "Z3JhZmFuYS1kZWZhdWx0LWVtYWls",
"namespace": "default",
"resourceVersion": "a82b34036bdabbc4",
"uid": "zyXFk301pvwNz4HRPrTMKPMFO2934cPB7H1ZXmyM1TUX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"settings": {
"addresses": "\u003cexample@email.com\u003e"
},
"type": "email",
"uid": "",
"version": "v1"
}
],
"title": "grafana-default-email"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "Z3JhZmFuYS1kZWZhdWx0LWVtYWlsdGVzdC1jcmVhdGUtZ2V0LWNvbmZpZw",
"namespace": "default",
"resourceVersion": "b2823b50ffa1eff6",
"uid": "JzW6DIlcxj4sRN8A2ULcwTXAmm0Vs0Z68aEBqXSvxK0X"
},
"spec": {
"integrations": [],
"title": "grafana-default-emailtest-create-get-config"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "ZGlzY29yZA",
"namespace": "default",
"resourceVersion": "06e437697f62ac59",
"uid": "8cH8Ql2S6VhPEVUhwlQEKYWyPbRJS7YKj2lEXdrehH8X"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"webhook_url": true
},
"settings": {
"http_config": {
"enable_http2": true,
"follow_redirects": false,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"message": "{{ template \"discord.default.message\" . }}",
"send_resolved": true,
"title": "{{ template \"discord.default.title\" . }}"
},
"type": "discord",
"uid": "",
"version": "v0mimir1"
}
],
"title": "discord"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "ZW1haWw",
"namespace": "default",
"resourceVersion": "9b3ffed277cee189",
"uid": "bhlvlN758xmnwVrHVPX0c5XvFHepenUbOXP0fuE6eUMX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"auth_password": true
},
"settings": {
"auth_username": "alertmanager",
"from": "alertmanager@example.com",
"headers": {
"Subject": "test subject"
},
"hello": "localhost",
"html": "{{ template \"email.default.html\" . }}",
"require_tls": true,
"send_resolved": true,
"smarthost": "smtp.example.com:587",
"text": "test email",
"tls_config": {
"insecure_skip_verify": false
},
"to": "team@example.com"
},
"type": "email",
"uid": "",
"version": "v0mimir1"
}
],
"title": "email"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "amlyYQ",
"namespace": "default",
"resourceVersion": "deae9d34f8554205",
"uid": "7Pu4xcRXbvw4XEX279SoqyO8Ibo8cMl0vAJyYTsJ0NEX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"http_config.basic_auth.password": true
},
"settings": {
"api_url": "http://localhost/jira",
"custom_fields": {
"customfield_10000": "test customfield_10000"
},
"description": "{{ template \"jira.default.description\" . }}",
"http_config": {
"basic_auth": {
"username": "alertmanager@example.com"
},
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"issue_type": "Bug",
"labels": [
"alertmanager",
"{{ .CommonLabels.severity }}"
],
"priority": "{{ template \"jira.default.priority\" . }}",
"project": "PROJ",
"send_resolved": true,
"summary": "{{ template \"jira.default.summary\" . }}"
},
"type": "jira",
"uid": "",
"version": "v0mimir1"
}
],
"title": "jira"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "bXN0ZWFtcw",
"namespace": "default",
"resourceVersion": "95c8d082d65466a3",
"uid": "z7xTMDjrk1HAHXPEx78tQb63LXYA6ivXLOtz2Z09ucIX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"webhook_url": true
},
"settings": {
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": "http://proxy.example.com:8080",
"tls_config": {
"insecure_skip_verify": false
}
},
"send_resolved": true,
"summary": "{{ template \"msteams.default.summary\" . }}",
"text": "{{ template \"msteams.default.text\" . }}",
"title": "{{ template \"msteams.default.title\" . }}"
},
"type": "teams",
"uid": "",
"version": "v0mimir1"
}
],
"title": "msteams"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "b3BzZ2VuaWU",
"namespace": "default",
"resourceVersion": "8ee2957ba150ba16",
"uid": "XmkZ214Dj030hvynYiwNLq8i6uRCjUYXMXjE5m19OKAX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"api_key": true
},
"settings": {
"actions": "test actions",
"api_url": "http://localhost/opsgenie/",
"description": "test description",
"details": {
"firing": "test firing"
},
"entity": "test entity",
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"message": "test message",
"note": "Triggered by Alertmanager",
"priority": "P3",
"responders": [
{
"name": "ops-team",
"type": "team"
}
],
"send_resolved": true,
"source": "Alertmanager",
"tags": "test-tags",
"update_alerts": true
},
"type": "opsgenie",
"uid": "",
"version": "v0mimir1"
}
],
"title": "opsgenie"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "1",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "cGFnZXJkdXR5",
"namespace": "default",
"resourceVersion": "fe673d5dcd67ccf0",
"uid": "QNitkUCkwzrIc7WVCCJGGDyvXLyo9csSUVqfyStyctQX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"routing_key": true,
"service_key": true
},
"settings": {
"class": "test class",
"client": "Alertmanager",
"client_url": "https://monitoring.example.com",
"component": "test component",
"description": "test description",
"details": {
"firing": "test firing",
"num_firing": "{{ .Alerts.Firing | len }}",
"num_resolved": "{{ .Alerts.Resolved | len }}",
"resolved": "{{ template \"pagerduty.default.instances\" .Alerts.Resolved }}"
},
"group": "test group",
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"images": [
{
"alt": "test alt",
"href": "http://localhost",
"src": "test src"
}
],
"links": [
{
"href": "http://localhost",
"text": "test text"
}
],
"send_resolved": true,
"severity": "test severity",
"source": "test source",
"url": "http://localhost/pagerduty"
},
"type": "pagerduty",
"uid": "",
"version": "v0mimir1"
}
],
"title": "pagerduty"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "cHVzaG92ZXI",
"namespace": "default",
"resourceVersion": "6ae076725ab463e0",
"uid": "t2TJSktI6vyGfdbLOKmxH4eBqgcIGsAuW8Qm9m0HRycX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"http_config.authorization.credentials": true,
"token": true,
"user_key": true
},
"settings": {
"expire": "1h0m0s",
"http_config": {
"authorization": {
"type": "Bearer"
},
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"message": "{{ template \"pushover.default.message\" . }}",
"priority": "{{ if eq .Status \"firing\" }}2{{ else }}0{{ end }}",
"retry": "30s",
"send_resolved": true,
"title": "{{ template \"pushover.default.title\" . }}",
"url": "http://localhost/pushover"
},
"type": "pushover",
"uid": "",
"version": "v0mimir1"
}
],
"title": "pushover"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "c2xhY2s",
"namespace": "default",
"resourceVersion": "ec0e343029ff5d8b",
"uid": "xSB0hnoc9j1CnLCHR3VgeVGXdVXILM0p2dM64bbHN9oX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"api_url": true
},
"settings": {
"actions": [
{
"confirm": {
"dismiss_text": "test-dismiss-text",
"ok_text": "test-ok-text",
"text": "test-text",
"title": "test-title"
},
"name": "test-name",
"style": "test-style",
"text": "test-text",
"type": "test-type",
"value": "test-value"
}
],
"callback_id": "test callback id",
"channel": "#alerts",
"color": "danger",
"fallback": "test fallback",
"fields": [
{
"short": true,
"title": "test title",
"value": "test value"
}
],
"footer": "test footer",
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"icon_emoji": ":warning:",
"icon_url": "https://example.com/icon.png",
"image_url": "https://example.com/image.png",
"link_names": true,
"mrkdwn_in": [
"fallback",
"pretext",
"text"
],
"pretext": "test pretext",
"send_resolved": true,
"text": "test text",
"thumb_url": "https://example.com/thumb.png",
"title": "test title",
"title_link": "http://localhost",
"username": "Alerting Team"
},
"type": "slack",
"uid": "",
"version": "v0mimir1"
}
],
"title": "slack"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "c25z",
"namespace": "default",
"resourceVersion": "77d734ad4c196d36",
"uid": "vSP8NtFr23hnqZqLxRgzUKfr1wOemOvZm1S6MYkfRI4X"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"sigv4.SecretKey": true
},
"settings": {
"attributes": {
"key1": "value1"
},
"http_config": {
"enable_http2": false,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"message": "{{ template \"sns.default.message\" . }}",
"send_resolved": true,
"sigv4": {
"AccessKey": "secret-access-key",
"Profile": "default,",
"Region": "us-east-1",
"RoleARN": "arn:aws:iam::123456789012:role/role-name"
},
"subject": "{{ template \"sns.default.subject\" . }}",
"topic_arn": "arn:aws:sns:us-east-1:123456789012:alerts"
},
"type": "sns",
"uid": "",
"version": "v0mimir1"
}
],
"title": "sns"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "dGVsZWdyYW0",
"namespace": "default",
"resourceVersion": "d9850878a33e302e",
"uid": "XLWjtmYcjP5PiqBCwZXX3YKHV1G8niRtpCakIpcHqoYX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"token": true
},
"settings": {
"api_url": "http://localhost/telegram-default",
"chat": -1001234567890,
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"message": "{{ template \"telegram.default.message\" . }}",
"parse_mode": "MarkdownV2",
"send_resolved": true
},
"type": "telegram",
"uid": "",
"version": "v0mimir1"
}
],
"title": "telegram"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "dmljdG9yb3Bz",
"namespace": "default",
"resourceVersion": "1e6886531440afc2",
"uid": "EWiwQ6TIW0GpEo46WusW7Nvg0HuD4QAbHf0JZ2OSOhEX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"api_key": true
},
"settings": {
"api_url": "http://localhost/victorops-default/",
"entity_display_name": "{{ template \"victorops.default.entity_display_name\" . }}",
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false,
"server_name": "victorops.com"
}
},
"message_type": "CRITICAL",
"monitoring_tool": "{{ template \"victorops.default.monitoring_tool\" . }}",
"routing_key": "team1",
"send_resolved": true,
"state_message": "{{ template \"victorops.default.state_message\" . }}"
},
"type": "victorops",
"uid": "",
"version": "v0mimir1"
}
],
"title": "victorops"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "d2ViZXg",
"namespace": "default",
"resourceVersion": "08fc955a08dfe9c0",
"uid": "wDNufI44UXHWq4ERRYenZ7XgXVV3Tjxaokz9IjMRZ54X"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"http_config.authorization.credentials": true
},
"settings": {
"api_url": "http://localhost/webes-default",
"http_config": {
"authorization": {
"type": "Bearer"
},
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"message": "{{ template \"webex.default.message\" . }}",
"room_id": "Y2lzY29zcGFyazovL3VzL1JPT00v12345678",
"send_resolved": true
},
"type": "webex",
"uid": "",
"version": "v0mimir1"
}
],
"title": "webex"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "1",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "d2ViaG9vaw",
"namespace": "default",
"resourceVersion": "494392f899a7b410",
"uid": "aKzigXATPp6HOh20yTrlTcuF2Y9IrPHridGIcWrJygsX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"url": true
},
"settings": {
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"max_alerts": 10,
"send_resolved": true,
"timeout": "0s",
"url_file": ""
},
"type": "webhook",
"uid": "",
"version": "v0mimir1"
}
],
"title": "webhook"
}
},
{
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
"kind": "Receiver",
"metadata": {
"annotations": {
"grafana.com/access/canReadSecrets": "true",
"grafana.com/canUse": "false",
"grafana.com/inUse/routes": "0",
"grafana.com/inUse/rules": "0",
"grafana.com/provenance": "converted_prometheus"
},
"name": "d2VjaGF0",
"namespace": "default",
"resourceVersion": "135913515cbc156b",
"uid": "jkXCvNrNVw7XX5nmYFyrGiA4ckAvJ282u2scW8KZq7IX"
},
"spec": {
"integrations": [
{
"disableResolveMessage": false,
"secureFields": {
"api_secret": true
},
"settings": {
"agent_id": "1000002",
"api_url": "http://localhost/wechat/",
"corp_id": "12345",
"http_config": {
"enable_http2": true,
"follow_redirects": true,
"proxy_url": null,
"tls_config": {
"insecure_skip_verify": false
}
},
"message": "test message",
"message_type": "text",
"send_resolved": true,
"to_party": "party1",
"to_tag": "tag1",
"to_user": "user1"
},
"type": "wechat",
"uid": "",
"version": "v0mimir1"
}
],
"title": "wechat"
}
}
],
"kind": "ReceiverList",
"metadata": {}
}
@@ -0,0 +1,247 @@
global:
resolve_timeout: 5m
smtp_smarthost: localhost:1025
smtp_from: alertmanager@example.com
smtp_auth_username: alertmanager
smtp_auth_password: password123
smtp_require_tls: false
smtp_hello: localhost
http_config:
follow_redirects: true
enable_http2: true
pagerduty_url: http://localhost/pagerduty-default
opsgenie_api_key: opsgenie-api-key-secret
opsgenie_api_url: http://localhost/opsgenie-default
victorops_api_key: victorops-api-key-secret
victorops_api_url: http://localhost/victorops-default
slack_api_url: http://localhost/slack-default
wechat_api_url: http://localhost/wechat-default
wechat_api_secret: wechat-api-secret
wechat_api_corp_id: corp123
telegram_api_url: http://localhost/telegram-default
webex_api_url: http://localhost/webes-default
jira_api_url: http://localhost/jira-default
route:
receiver: webhook
group_by:
- alertname
- cluster
group_wait: 1s
group_interval: 5s
repeat_interval: 60s
routes:
- matchers:
- severity="critical"
receiver: pagerduty
receivers:
- name: grafana-default-email
- name: discord
discord_configs:
- webhook_url: http://localhost/discord
send_resolved: true
http_config:
follow_redirects: false
enable_http2: true
title: '{{ template "discord.default.title" . }}'
message: '{{ template "discord.default.message" . }}'
- name: email
email_configs:
- to: team@example.com
from: alertmanager@example.com
smarthost: smtp.example.com:587
auth_username: alertmanager
auth_password: password123
require_tls: true
send_resolved: true
text: test email
hello: localhost
headers:
Subject: test subject
- name: jira
jira_configs:
- api_url: http://localhost/jira
send_resolved: true
http_config:
basic_auth:
username: alertmanager@example.com
password: pat123
project: PROJ
issue_type: Bug
summary: '{{ template "jira.default.summary" . }}'
description: '{{ template "jira.default.description" . }}'
priority: '{{ template "jira.default.priority" . }}'
labels:
- alertmanager
- '{{ .CommonLabels.severity }}'
fields:
customfield_10000: test customfield_10000
- name: msteams
msteams_configs:
- webhook_url: http://localhost/msteams
send_resolved: true
http_config:
proxy_url: http://proxy.example.com:8080
title: '{{ template "msteams.default.title" . }}'
summary: '{{ template "msteams.default.summary" . }}'
text: '{{ template "msteams.default.text" . }}'
- name: opsgenie
opsgenie_configs:
- api_key: api-secret-key
api_url: http://localhost/opsgenie
message: test message
description: test description
source: Alertmanager
details:
firing: test firing
entity: test entity
responders:
- type: team
name: ops-team
actions: test actions
tags: test-tags
note: Triggered by Alertmanager
priority: P3
update_alerts: true
send_resolved: true
- name: pagerduty
pagerduty_configs:
- url: http://localhost/pagerduty
routing_key: test-routing-secret-key
service_key: test-service-secret-key
client: Alertmanager
client_url: https://monitoring.example.com
description: test description
severity: test severity
details:
firing: test firing
images:
- alt: test alt
src: test src
href: http://localhost
links:
- href: http://localhost
text: test text
source: test source
class: test class
component: test component
group: test group
send_resolved: true
- name: pushover
pushover_configs:
- user_key: secret-user-key
token: secret-token
send_resolved: true
http_config:
bearer_token: token123
title: '{{ template "pushover.default.title" . }}'
message: '{{ template "pushover.default.message" . }}'
url: http://localhost/pushover
priority: '{{ if eq .Status "firing" }}2{{ else }}0{{ end }}'
retry: 30s
expire: 1h
- name: slack
slack_configs:
- api_url: http://localhost/slack
channel: '#alerts'
username: Alerting Team
color: danger
title: test title
title_link: http://localhost
pretext: test pretext
text: test text
fields:
- title: test title
value: test value
short: true
short_fields: false
footer: test footer
fallback: test fallback
callback_id: test callback id
icon_emoji: ':warning:'
icon_url: https://example.com/icon.png
image_url: https://example.com/image.png
thumb_url: https://example.com/thumb.png
link_names: true
mrkdwn_in:
- fallback
- pretext
- text
actions:
- type: test-type
text: test-text
style: test-style
name: test-name
value: test-value
confirm:
title: test-title
text: test-text
ok_text: test-ok-text
dismiss_text: test-dismiss-text
send_resolved: true
- name: sns
sns_configs:
- topic_arn: arn:aws:sns:us-east-1:123456789012:alerts
send_resolved: true
sigv4:
region: us-east-1
access_key: secret-access-key
secret_key: secret-secret-key
profile: default,
role_arn: "arn:aws:iam::123456789012:role/role-name"
http_config:
enable_http2: false
subject: '{{ template "sns.default.subject" . }}'
message: '{{ template "sns.default.message" . }}'
attributes:
key1: value1
- name: telegram
telegram_configs:
- bot_token: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
chat_id: -1001234567890
send_resolved: true
http_config:
follow_redirects: true
enable_http2: true
message: '{{ template "telegram.default.message" . }}'
parse_mode: MarkdownV2
- name: victorops
victorops_configs:
- api_key: victorops-api-key-secret
send_resolved: true
http_config:
tls_config:
server_name: victorops.com
routing_key: team1
message_type: CRITICAL
entity_display_name: '{{ template "victorops.default.entity_display_name" . }}'
state_message: '{{ template "victorops.default.state_message" . }}'
monitoring_tool: '{{ template "victorops.default.monitoring_tool" . }}'
- name: webex
webex_configs:
- send_resolved: true
http_config:
authorization:
type: Bearer
credentials: webex-secret-token
room_id: Y2lzY29zcGFyazovL3VzL1JPT00v12345678
message: '{{ template "webex.default.message" . }}'
- name: webhook
webhook_configs:
- url: http://localhost/webhook
send_resolved: true
http_config:
follow_redirects: true
enable_http2: true
max_alerts: 10
- name: wechat
wechat_configs:
- send_resolved: true
api_url: http://localhost/wechat
api_secret: wechat-api-secret
corp_id: 12345
to_user: user1
to_party: party1
to_tag: tag1
agent_id: 1000002
message: test message
message_type: text