diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/ext.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/ext.go index 772e823528d..54ebe9ba8c1 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/ext.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/ext.go @@ -3,3 +3,4 @@ package v0alpha1 const InternalPrefix = "grafana.com/" const ProvenanceStatusAnnotationKey = InternalPrefix + "provenance" const ProvenanceStatusNone = "none" +const CanUseAnnotationKey = InternalPrefix + "canUse" diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_ext.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_ext.go index 73815d2250b..1e92a27d3a0 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_ext.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_ext.go @@ -35,6 +35,13 @@ func (o *Receiver) SetAccessControl(action string) { o.Annotations[AccessControlAnnotation(action)] = "true" } +func (o *Receiver) SetCanUse(canUse bool) { + if o.Annotations == nil { + o.Annotations = make(map[string]string, 1) + } + o.Annotations[CanUseAnnotationKey] = fmt.Sprintf("%v", canUse) +} + // AccessControlAnnotation returns the key for the access control annotation for the given action. // Ex. grafana.com/access/canDelete. func AccessControlAnnotation(action string) string { diff --git a/pkg/registry/apps/alerting/notifications/receiver/conversions.go b/pkg/registry/apps/alerting/notifications/receiver/conversions.go index 04b85a5af79..aca723b7fd7 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/conversions.go +++ b/pkg/registry/apps/alerting/notifications/receiver/conversions.go @@ -101,6 +101,7 @@ func convertToK8sResource( rules = append(rules, rule.UID) } r.SetInUse(metadata.InUseByRoutes, rules) + r.SetCanUse(metadata.CanUse) } r.UID = gapiutil.CalculateClusterWideUID(r) return r, nil @@ -120,6 +121,7 @@ func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[str Integrations: make([]*ngmodels.Integration, 0, len(receiver.Spec.Integrations)), Version: receiver.ResourceVersion, Provenance: ngmodels.ProvenanceNone, + Origin: ngmodels.ResourceOriginGrafana, // Set to Grafana by default. } storedSecureFields := make(map[string][]string, len(receiver.Spec.Integrations)) for _, integration := range receiver.Spec.Integrations { diff --git a/pkg/services/ngalert/models/receivers.go b/pkg/services/ngalert/models/receivers.go index 56c6ad06841..5f56f7a137a 100644 --- a/pkg/services/ngalert/models/receivers.go +++ b/pkg/services/ngalert/models/receivers.go @@ -37,8 +37,20 @@ type GetReceiversQuery struct { type ReceiverMetadata struct { InUseByRules []AlertRuleKey InUseByRoutes int + // CanUse is true if the receiver can be used in routes and rules. + CanUse bool } +// ResourceOrigin represents the origin or source of the resource. +type ResourceOrigin string + +const ( + // ResourceOriginGrafana indicates that the resource is in the Grafana configuration + ResourceOriginGrafana ResourceOrigin = "grafana" + // ResourceOriginImported indicates that the resource is from the imported configuration + ResourceOriginImported ResourceOrigin = "imported" +) + // Receiver is the domain model representation of a receiver / contact point. type Receiver struct { UID string @@ -46,6 +58,7 @@ type Receiver struct { Integrations []*Integration Provenance Provenance Version string + Origin ResourceOrigin } func (r *Receiver) Clone() Receiver { @@ -54,6 +67,7 @@ func (r *Receiver) Clone() Receiver { Name: r.Name, Provenance: r.Provenance, Version: r.Version, + Origin: r.Origin, } if r.Integrations != nil { diff --git a/pkg/services/ngalert/models/receivers_test.go b/pkg/services/ngalert/models/receivers_test.go index d3db9f89189..4f5ae6cc90b 100644 --- a/pkg/services/ngalert/models/receivers_test.go +++ b/pkg/services/ngalert/models/receivers_test.go @@ -378,6 +378,7 @@ func TestReceiver_Fingerprint(t *testing.T) { fingerprint := baseReceiver.Fingerprint() excludedFields := map[string]struct{}{ "Version": {}, + "Origin": {}, } reflectVal := reflect.ValueOf(&completelyDifferentReceiver).Elem() diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 53b72f3aae2..7f124644f34 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -1180,6 +1180,7 @@ func ReceiverGen(mutators ...Mutator[Receiver]) func() Receiver { Name: name, Integrations: []*Integration{&integration}, Provenance: ProvenanceNone, + Origin: ResourceOriginGrafana, } for _, mutator := range mutators { mutator(&c) @@ -1246,6 +1247,12 @@ func (n ReceiverMutators) Decrypted(fn DecryptFn) Mutator[Receiver] { } } +func (n ReceiverMutators) WithOrigin(origin ResourceOrigin) Mutator[Receiver] { + return func(r *Receiver) { + r.Origin = origin + } +} + // Integrations // CopyIntegrationWith creates a deep copy of Integration and then applies mutators to it. diff --git a/pkg/services/ngalert/notifier/legacy_storage/compat.go b/pkg/services/ngalert/notifier/legacy_storage/compat.go index ccc9f030933..0db0aa6e384 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/compat.go +++ b/pkg/services/ngalert/notifier/legacy_storage/compat.go @@ -65,19 +65,7 @@ func ReceiverToPostableApiReceiver(r *models.Receiver) (*apimodels.PostableApiRe }, nil } -func PostableApiReceiversToReceivers(postables []*apimodels.PostableApiReceiver, storedProvenances map[string]models.Provenance) ([]*models.Receiver, error) { - receivers := make([]*models.Receiver, 0, len(postables)) - for _, postable := range postables { - r, err := PostableApiReceiverToReceiver(postable, GetReceiverProvenance(storedProvenances, postable)) - if err != nil { - return nil, err - } - receivers = append(receivers, r) - } - return receivers, nil -} - -func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, provenance models.Provenance) (*models.Receiver, error) { +func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, provenance models.Provenance, origin models.ResourceOrigin) (*models.Receiver, error) { integrations, err := PostableGrafanaReceiversToIntegrations(postable.GrafanaManagedReceivers) if err != nil { return nil, err @@ -87,13 +75,18 @@ func PostableApiReceiverToReceiver(postable *apimodels.PostableApiReceiver, prov Name: postable.GetName(), Integrations: integrations, Provenance: provenance, + Origin: origin, } r.Version = r.Fingerprint() return r, nil } // GetReceiverProvenance determines the provenance of a definitions.PostableApiReceiver based on the provenance of its integrations. -func GetReceiverProvenance(storedProvenances map[string]models.Provenance, r *apimodels.PostableApiReceiver) models.Provenance { +func GetReceiverProvenance(storedProvenances map[string]models.Provenance, r *apimodels.PostableApiReceiver, origin models.ResourceOrigin) models.Provenance { + if origin == models.ResourceOriginImported { + return models.ProvenanceConvertedPrometheus + } + if len(r.GrafanaManagedReceivers) == 0 || len(storedProvenances) == 0 { return models.ProvenanceNone } diff --git a/pkg/services/ngalert/notifier/legacy_storage/receivers.go b/pkg/services/ngalert/notifier/legacy_storage/receivers.go index 6272a14b731..d791e6ca31c 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/receivers.go +++ b/pkg/services/ngalert/notifier/legacy_storage/receivers.go @@ -43,7 +43,7 @@ func (rev *ConfigRevision) CreateReceiver(receiver *models.Receiver) (*models.Re return nil, err } - return PostableApiReceiverToReceiver(postable, receiver.Provenance) + return PostableApiReceiverToReceiver(postable, receiver.Provenance, receiver.Origin) } func (rev *ConfigRevision) UpdateReceiver(receiver *models.Receiver) (*models.Receiver, error) { @@ -69,7 +69,7 @@ func (rev *ConfigRevision) UpdateReceiver(receiver *models.Receiver) (*models.Re return nil, err } - return PostableApiReceiverToReceiver(newReceiver, receiver.Provenance) + return PostableApiReceiverToReceiver(newReceiver, receiver.Provenance, receiver.Origin) } // ReceiverNameUsedByRoutes checks if a receiver name is used in any routes. @@ -89,7 +89,7 @@ func (rev *ConfigRevision) GetReceiver(uid string, prov Provenances) (*models.Re if NameToUid(r.GetName()) != uid { continue } - recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r)) + recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r, models.ResourceOriginGrafana), models.ResourceOriginGrafana) if err != nil { return nil, fmt.Errorf("failed to convert receiver %q: %w", r.Name, err) } @@ -109,7 +109,7 @@ func (rev *ConfigRevision) GetReceivers(uids []string, prov Provenances) ([]*mod if len(uids) > 0 && !slices.Contains(uids, uid) { continue } - recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r)) + recv, err := PostableApiReceiverToReceiver(r, GetReceiverProvenance(prov, r, models.ResourceOriginGrafana), models.ResourceOriginGrafana) if err != nil { return nil, fmt.Errorf("failed to convert receiver %q: %w", r.Name, err) } @@ -131,7 +131,7 @@ func DecryptedReceivers(receivers []*definitions.PostableApiReceiver, decryptFn decrypted := make([]*definitions.PostableApiReceiver, len(receivers)) for i, r := range receivers { // We don't care about the provenance here, so we pass ProvenanceNone. - rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone) + rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone, models.ResourceOriginGrafana) if err != nil { return nil, err } @@ -154,7 +154,7 @@ func EncryptedReceivers(receivers []*definitions.PostableApiReceiver, encryptFn encrypted := make([]*definitions.PostableApiReceiver, len(receivers)) for i, r := range receivers { // We don't care about the provenance here, so we pass ProvenanceNone. - rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone) + rcv, err := PostableApiReceiverToReceiver(r, models.ProvenanceNone, models.ResourceOriginGrafana) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index a2332cbe6cd..d25f735fcd7 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -36,6 +36,11 @@ var ( "Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}", errutil.WithPublic(`Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}. You must update those resources first using the original provision method.`), ) + + ErrReceiverOrigin = errutil.BadRequest("alerting.notifications.receivers.originInvalid").MustTemplate( + "Receiver '{{ .Public.Name }} cannot be {{ .Public.Action }}d because it belongs to an imported configuration.", + errutil.WithPublic("Receiver '{{ .Public.Name }} cannot be {{ .Public.Action }}d because it belongs to an imported configuration. Finish the import of the configuration first."), + ) ) // ReceiverService is the service for managing alertmanager receivers. @@ -271,6 +276,10 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle return err } + if existing.Origin != models.ResourceOriginGrafana { + return makeErrReceiverOrigin(existing, "delete") + } + logger := rs.log.FromContext(ctx).New("receiver", existing.Name, "uid", uid, "version", version, "integrations", existing.GetIntegrationTypes()) // Check optimistic concurrency. @@ -329,7 +338,9 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive if err := rs.authz.AuthorizeCreate(ctx, user); err != nil { return nil, err } - + if r.Origin != models.ResourceOriginGrafana { + return nil, makeErrReceiverOrigin(r, "create") + } revision, err := rs.cfgStore.Get(ctx, orgID) if err != nil { return nil, err @@ -429,6 +440,10 @@ 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 } @@ -538,6 +553,7 @@ func (rs *ReceiverService) InUseMetadata(ctx context.Context, orgID int64, recei results[rcv.GetUID()] = models.ReceiverMetadata{ InUseByRoutes: receiverUses[rcv.Name], InUseByRules: byReceiver[rcv.Name], + CanUse: rcv.Origin == models.ResourceOriginGrafana, // Only receivers from the Grafana configuration can be used. } } @@ -684,6 +700,10 @@ func makeErrReceiverDependentResourcesProvenance(usedByRoutes bool, rules []mode }) } +func makeErrReceiverOrigin(r *models.Receiver, action string) error { + return ErrReceiverOrigin.Build(errutil.TemplateData{Public: map[string]interface{}{"Action": action, "Name": r.Name}}) +} + func (rs *ReceiverService) RenameReceiverInDependentResources(ctx context.Context, orgID int64, revision *legacy_storage.ConfigRevision, oldName, newName string, receiverProvenance models.Provenance) error { ctx, span := rs.tracer.Start(ctx, "alerting.receivers.rename-dependent-resources", trace.WithAttributes( attribute.String("oldName", oldName), diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index f7d2a384962..5cf9146bb1c 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -404,6 +404,12 @@ func TestReceiverService_Create(t *testing.T) { ), models.ReceiverMuts.Encrypted(models.Base64Enrypt)), expectedProvenances: map[string]models.Provenance{generated(0): models.ProvenanceNone, generated(1): models.ProvenanceNone}, // Mark UIDs as generated so that test will insert generated UID. }, + { + name: "create receiver with non-Grafana origin fails", + user: writer, + receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithOrigin(models.ResourceOriginImported)), + expectedErr: ErrReceiverOrigin, + }, { name: "create integration with invalid UID fails", user: writer, @@ -1479,18 +1485,22 @@ func TestReceiverService_InUseMetadata(t *testing.T) { legacy_storage.NameToUid("receiver1"): { InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule1uid"}}, InUseByRoutes: 2, + CanUse: true, }, legacy_storage.NameToUid("receiver2"): { InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule1uid"}, {OrgID: 1, UID: "rule2uid"}}, InUseByRoutes: 1, + CanUse: true, }, legacy_storage.NameToUid("receiver3"): { InUseByRules: []models.AlertRuleKey{{OrgID: 1, UID: "rule2uid"}}, InUseByRoutes: 2, + CanUse: true, }, legacy_storage.NameToUid("receiver4"): { InUseByRules: []models.AlertRuleKey{}, InUseByRoutes: 1, + CanUse: true, }, }, }, diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 16ae89f7dff..9c01e08ae26 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -608,6 +608,7 @@ func TestIntegrationAccessControl(t *testing.T) { // Set expected metadata. expectedWithMetadata := expected.Copy().(*v0alpha1.Receiver) expectedWithMetadata.SetInUse(0, nil) + expectedWithMetadata.SetCanUse(true) if tc.canUpdate { expectedWithMetadata.SetAccessControl("canWrite") } @@ -1294,6 +1295,7 @@ func TestIntegrationCRUD(t *testing.T) { receiver.SetAccessControl("canReadSecrets") receiver.SetAccessControl("canAdmin") receiver.SetInUse(0, nil) + receiver.SetCanUse(true) // Use export endpoint because it's the only way to get decrypted secrets fast. cliCfg := helper.Org1.Admin.NewRestConfig()