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:
@@ -2215,6 +2215,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
|
||||
env.log,
|
||||
ngalertfakes.NewFakeReceiverPermissionsService(),
|
||||
tracer,
|
||||
false,
|
||||
)
|
||||
return ProvisioningSrv{
|
||||
log: env.log,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
+818
@@ -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
|
||||
Reference in New Issue
Block a user