diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index db380834c4a..830c2f83434 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -2215,6 +2215,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi env.log, ngalertfakes.NewFakeReceiverPermissionsService(), tracer, + false, ) return ProvisioningSrv{ log: env.log, diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 321f13199d7..6fb624f52a5 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -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. diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 6de3f7b1ba0..299a511c35e 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -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 diff --git a/pkg/services/ngalert/notifier/legacy_storage/compat.go b/pkg/services/ngalert/notifier/legacy_storage/compat.go index ea93830d5fb..bd1946ba109 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/compat.go +++ b/pkg/services/ngalert/notifier/legacy_storage/compat.go @@ -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 { diff --git a/pkg/services/ngalert/notifier/legacy_storage/compat_test.go b/pkg/services/ngalert/notifier/legacy_storage/compat_test.go new file mode 100644 index 00000000000..c239b69b9b2 --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/compat_test.go @@ -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) + }) +} diff --git a/pkg/services/ngalert/notifier/legacy_storage/imported.go b/pkg/services/ngalert/notifier/legacy_storage/imported.go new file mode 100644 index 00000000000..9657e93daa6 --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/imported.go @@ -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 +} diff --git a/pkg/services/ngalert/notifier/legacy_storage/imported_test.go b/pkg/services/ngalert/notifier/legacy_storage/imported_test.go new file mode 100644 index 00000000000..8a8ff95476b --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/imported_test.go @@ -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/" +` diff --git a/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go index 528f5c6d518..bf362ec0eac 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go +++ b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go @@ -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 } diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index d25f735fcd7..56a68fb3e55 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -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 +} diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 221f3e805d0..8d4b7630436 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -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 { diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index ddddadb1104..3d54f640c56 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -570,6 +570,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec log.NewNopLogger(), fakes.NewFakeReceiverPermissionsService(), tracing.InitializeTracerForTest(), + false, ) return NewContactPointService( diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index e7e8c3c5fa7..c79a9625d50 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -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) diff --git a/pkg/tests/apis/alerting/notifications/receivers/imported_test.go b/pkg/tests/apis/alerting/notifications/receivers/imported_test.go new file mode 100644 index 00000000000..f265834b229 --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/receivers/imported_test.go @@ -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{}{} + } + } +} diff --git a/pkg/tests/apis/alerting/notifications/receivers/test-data/imported-expected-snapshot.json b/pkg/tests/apis/alerting/notifications/receivers/test-data/imported-expected-snapshot.json new file mode 100644 index 00000000000..621f9c3af16 --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/receivers/test-data/imported-expected-snapshot.json @@ -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": {} +} diff --git a/pkg/tests/apis/alerting/notifications/receivers/test-data/imported.yaml b/pkg/tests/apis/alerting/notifications/receivers/test-data/imported.yaml new file mode 100644 index 00000000000..5d4e8fd927f --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/receivers/test-data/imported.yaml @@ -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 \ No newline at end of file