@ -5,13 +5,17 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"slices"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
"github.com/grafana/grafana/pkg/services/secrets"
)
@ -22,6 +26,11 @@ var (
ErrNotFound = errors . New ( "not found" ) // TODO: convert to errutil
)
var (
ErrReceiverInUse = errutil . Conflict ( "alerting.notifications.receiver.used" , errutil . WithPublicMessage ( "Receiver is used by one or many notification policies" ) )
ErrVersionConflict = errutil . Conflict ( "alerting.notifications.receiver.conflict" )
)
// ReceiverService is the service for managing alertmanager receivers.
type ReceiverService struct {
ac accesscontrol . AccessControl
@ -30,6 +39,7 @@ type ReceiverService struct {
encryptionService secrets . Service
xact transactionManager
log log . Logger
validator validation . ProvenanceStatusTransitionValidator
}
type configStore interface {
@ -39,6 +49,7 @@ type configStore interface {
type provisoningStore interface {
GetProvenances ( ctx context . Context , org int64 , resourceType string ) ( map [ string ] models . Provenance , error )
DeleteProvenance ( ctx context . Context , o models . Provisionable , org int64 ) error
}
type transactionManager interface {
@ -60,6 +71,7 @@ func NewReceiverService(
encryptionService : encryptionService ,
xact : xact ,
log : log ,
validator : validation . ValidateProvenanceRelaxed ,
}
}
@ -119,7 +131,7 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver
return definitions . GettableApiReceiver { } , err
}
provenances , err := rs . provisioningStore . GetProvenances ( ctx , q . OrgID , "contactPoint" )
provenances , err := rs . provisioningStore . GetProvenances ( ctx , q . OrgID , ( & definitions . EmbeddedContactPoint { } ) . ResourceType ( ) )
if err != nil {
return definitions . GettableApiReceiver { } , err
}
@ -158,7 +170,7 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
return nil , err
}
provenances , err := rs . provisioningStore . GetProvenances ( ctx , q . OrgID , "contactPoint" )
provenances , err := rs . provisioningStore . GetProvenances ( ctx , q . OrgID , ( & definitions . EmbeddedContactPoint { } ) . ResourceType ( ) )
if err != nil {
return nil , err
}
@ -213,6 +225,83 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
return output , nil
}
// DeleteReceiver deletes a receiver by uid.
// UID field currently does not exist, we assume the uid is a particular hashed value of the receiver name.
func ( rs * ReceiverService ) DeleteReceiver ( ctx context . Context , uid string , orgID int64 , callerProvenance definitions . Provenance , version string ) error {
//TODO: Check delete permissions.
baseCfg , err := rs . cfgStore . GetLatestAlertmanagerConfiguration ( ctx , orgID )
if err != nil {
return err
}
cfg := definitions . PostableUserConfig { }
err = json . Unmarshal ( [ ] byte ( baseCfg . AlertmanagerConfiguration ) , & cfg )
if err != nil {
return err
}
idx , recv := getReceiverByUID ( cfg , uid )
if recv == nil {
return ErrNotFound // TODO: nil?
}
// TODO: Implement + check optimistic concurrency.
storedProvenance , err := rs . getContactPointProvenance ( ctx , recv , orgID )
if err != nil {
return err
}
if err := rs . validator ( storedProvenance , models . Provenance ( callerProvenance ) ) ; err != nil {
return err
}
if isReceiverInUse ( recv . Name , [ ] * definitions . Route { cfg . AlertmanagerConfig . Route } ) {
return ErrReceiverInUse . Errorf ( "" )
}
// Remove the receiver from the configuration.
cfg . AlertmanagerConfig . Receivers = append ( cfg . AlertmanagerConfig . Receivers [ : idx ] , cfg . AlertmanagerConfig . Receivers [ idx + 1 : ] ... )
return rs . xact . InTransaction ( ctx , func ( ctx context . Context ) error {
serialized , err := json . Marshal ( cfg )
if err != nil {
return err
}
cmd := models . SaveAlertmanagerConfigurationCmd {
AlertmanagerConfiguration : string ( serialized ) ,
ConfigurationVersion : baseCfg . ConfigurationVersion ,
FetchedConfigurationHash : baseCfg . ConfigurationHash ,
Default : false ,
OrgID : orgID ,
}
err = rs . cfgStore . UpdateAlertmanagerConfiguration ( ctx , & cmd )
if err != nil {
return err
}
// Remove provenance for all integrations in the receiver.
for _ , integration := range recv . GrafanaManagedReceivers {
target := definitions . EmbeddedContactPoint { UID : integration . UID }
if err := rs . provisioningStore . DeleteProvenance ( ctx , & target , orgID ) ; err != nil {
return err
}
}
return nil
} )
}
func ( rs * ReceiverService ) CreateReceiver ( ctx context . Context , r definitions . GettableApiReceiver , orgID int64 ) ( definitions . GettableApiReceiver , error ) {
// TODO: Stub
panic ( "not implemented" )
}
func ( rs * ReceiverService ) UpdateReceiver ( ctx context . Context , r definitions . GettableApiReceiver , orgID int64 ) ( definitions . GettableApiReceiver , error ) {
// TODO: Stub
panic ( "not implemented" )
}
func ( rs * ReceiverService ) decryptOrRedact ( ctx context . Context , decrypt bool , name , fallback string ) func ( value string ) string {
return func ( value string ) string {
if ! decrypt {
@ -232,3 +321,61 @@ func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, na
return string ( decrypted )
}
}
// getContactPointProvenance determines the provenance of a definitions.PostableApiReceiver based on the provenance of its integrations.
func ( rs * ReceiverService ) getContactPointProvenance ( ctx context . Context , r * definitions . PostableApiReceiver , orgID int64 ) ( models . Provenance , error ) {
if len ( r . GrafanaManagedReceivers ) == 0 {
return models . ProvenanceNone , nil
}
storedProvenances , err := rs . provisioningStore . GetProvenances ( ctx , orgID , ( & definitions . EmbeddedContactPoint { } ) . ResourceType ( ) )
if err != nil {
return "" , err
}
// Current provisioning works on the integration level, so we need some way to determine the provenance of the
// entire receiver. All integrations in a receiver should have the same provenance, but we don't want to rely on
// this assumption in case the first provenance is None and a later one is not. To this end, we return the first
// non-zero provenance we find.
for _ , contactPoint := range r . GrafanaManagedReceivers {
if p , exists := storedProvenances [ contactPoint . UID ] ; exists && p != models . ProvenanceNone {
return p , nil
}
}
return models . ProvenanceNone , nil
}
// getReceiverByUID returns the index and receiver with the given UID.
func getReceiverByUID ( cfg definitions . PostableUserConfig , uid string ) ( int , * definitions . PostableApiReceiver ) {
for i , r := range cfg . AlertmanagerConfig . Receivers {
if getUID ( r ) == uid {
return i , r
}
}
return 0 , nil
}
// getUID returns the UID of a PostableApiReceiver.
// Currently, the UID is a hash of the receiver name.
func getUID ( t * definitions . PostableApiReceiver ) string { // TODO replace to stable UID when we switch to normal storage
sum := fnv . New64 ( )
_ , _ = sum . Write ( [ ] byte ( t . Name ) )
return fmt . Sprintf ( "%016x" , sum . Sum64 ( ) )
}
// TODO: Check if the contact point is used directly in an alert rule.
// isReceiverInUse checks if a receiver is used in a route or any of its sub-routes.
func isReceiverInUse ( name string , routes [ ] * definitions . Route ) bool {
if len ( routes ) == 0 {
return false
}
for _ , route := range routes {
if route . Receiver == name {
return true
}
if isReceiverInUse ( name , route . Routes ) {
return true
}
}
return false
}