Alerting: Receiver API complete core implementation (#91738)
* Replace global authz abstraction with one compatible with uid scope * Replace GettableApiReceiver with models.Receiver in receiver_svc * GrafanaIntegrationConfig -> models.Integration * Implement Create/Update methods * Add optimistic concurrency to receiver API * Add scope to ReceiversRead & ReceiversReadSecrets migrates existing permissions to include implicit global scope * Add receiver create, update, delete actions * Check if receiver is used by rules before delete * On receiver name change update in routes and notification settings * Improve errors * Linting * Include read permissions are requirements for create/update/delete * Alias ngalert/models to ngmodels to differentiate from v0alpha1 model * Ensure integration UIDs are valid, unique, and generated if empty * Validate integration settings on create/update * Leverage UidToName to GetReceiver instead of GetReceivers * Remove some unnecessary uses of simplejson * alerting.notifications.receiver -> alerting.notifications.receivers * validator -> provenanceValidator * Only validate the modified receiver stops existing invalid receivers from preventing modification of a valid receiver. * Improve error in Integration.Encrypt * Remove scope from alert.notifications.receivers:create * Add todos for receiver renaming * Use receiverAC precondition checks in k8s api * Linting * Optional optimistic concurrency for delete * make update-workspace * More specific auth checks in k8s authorize.go * Add debug log when delete optimistic concurrency is skipped * Improve error message on authorizer.DecisionDeny * Keep error for non-forbidden errutil errors
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
@@ -10,11 +11,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
amv2 "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
alertingModels "github.com/grafana/alerting/models"
|
||||
|
||||
@@ -1092,6 +1095,220 @@ func (n SilenceMutators) WithEmptyId() Mutator[Silence] {
|
||||
}
|
||||
}
|
||||
|
||||
// Receivers
|
||||
|
||||
// CopyReceiverWith creates a deep copy of Receiver and then applies mutators to it.
|
||||
func CopyReceiverWith(r Receiver, mutators ...Mutator[Receiver]) Receiver {
|
||||
c := r.Clone()
|
||||
for _, mutator := range mutators {
|
||||
mutator(&c)
|
||||
}
|
||||
c.Version = c.Fingerprint()
|
||||
return c
|
||||
}
|
||||
|
||||
// ReceiverGen generates Receiver using a base and mutators.
|
||||
func ReceiverGen(mutators ...Mutator[Receiver]) func() Receiver {
|
||||
return func() Receiver {
|
||||
name := util.GenerateShortUID()
|
||||
integration := IntegrationGen(IntegrationMuts.WithName(name))()
|
||||
c := Receiver{
|
||||
UID: nameToUid(name),
|
||||
Name: name,
|
||||
Integrations: []*Integration{&integration},
|
||||
Provenance: ProvenanceNone,
|
||||
}
|
||||
for _, mutator := range mutators {
|
||||
mutator(&c)
|
||||
}
|
||||
c.Version = c.Fingerprint()
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ReceiverMuts = ReceiverMutators{}
|
||||
)
|
||||
|
||||
type ReceiverMutators struct{}
|
||||
|
||||
func (n ReceiverMutators) WithName(name string) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
r.Name = name
|
||||
r.UID = nameToUid(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (n ReceiverMutators) WithProvenance(provenance Provenance) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
r.Provenance = provenance
|
||||
}
|
||||
}
|
||||
|
||||
func (n ReceiverMutators) WithValidIntegration(integrationType string) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
integration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
|
||||
r.Integrations = []*Integration{&integration}
|
||||
}
|
||||
}
|
||||
|
||||
func (n ReceiverMutators) WithInvalidIntegration(integrationType string) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
integration := IntegrationGen(IntegrationMuts.WithInvalidConfig(integrationType))()
|
||||
r.Integrations = []*Integration{&integration}
|
||||
}
|
||||
}
|
||||
|
||||
func (n ReceiverMutators) WithIntegrations(integration ...Integration) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
integrations := make([]*Integration, len(integration))
|
||||
for i, v := range integration {
|
||||
clone := v.Clone()
|
||||
integrations[i] = &clone
|
||||
}
|
||||
r.Integrations = integrations
|
||||
}
|
||||
}
|
||||
|
||||
func (n ReceiverMutators) Encrypted(fn EncryptFn) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
_ = r.Encrypt(fn)
|
||||
}
|
||||
}
|
||||
func (n ReceiverMutators) Decrypted(fn DecryptFn) Mutator[Receiver] {
|
||||
return func(r *Receiver) {
|
||||
_ = r.Decrypt(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// Integrations
|
||||
|
||||
// CopyIntegrationWith creates a deep copy of Integration and then applies mutators to it.
|
||||
func CopyIntegrationWith(r Integration, mutators ...Mutator[Integration]) Integration {
|
||||
c := r.Clone()
|
||||
for _, mutator := range mutators {
|
||||
mutator(&c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// IntegrationGen generates Integration using a base and mutators.
|
||||
func IntegrationGen(mutators ...Mutator[Integration]) func() Integration {
|
||||
return func() Integration {
|
||||
name := util.GenerateShortUID()
|
||||
randomIntegrationType, _ := randomMapKey(alertingNotify.AllKnownConfigsForTesting)
|
||||
|
||||
c := Integration{
|
||||
UID: util.GenerateShortUID(),
|
||||
Name: name,
|
||||
DisableResolveMessage: rand.Intn(2) == 1,
|
||||
Settings: make(map[string]any),
|
||||
SecureSettings: make(map[string]string),
|
||||
}
|
||||
|
||||
IntegrationMuts.WithValidConfig(randomIntegrationType)(&c)
|
||||
|
||||
for _, mutator := range mutators {
|
||||
mutator(&c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
IntegrationMuts = IntegrationMutators{}
|
||||
Base64Enrypt = func(s string) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString([]byte(s)), nil
|
||||
}
|
||||
Base64Decrypt = func(s string) (string, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
return string(b), err
|
||||
}
|
||||
)
|
||||
|
||||
type IntegrationMutators struct{}
|
||||
|
||||
func (n IntegrationMutators) WithUID(uid string) Mutator[Integration] {
|
||||
return func(s *Integration) {
|
||||
s.UID = uid
|
||||
}
|
||||
}
|
||||
|
||||
func (n IntegrationMutators) WithName(name string) Mutator[Integration] {
|
||||
return func(s *Integration) {
|
||||
s.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func (n IntegrationMutators) WithValidConfig(integrationType string) Mutator[Integration] {
|
||||
return func(c *Integration) {
|
||||
config := alertingNotify.AllKnownConfigsForTesting[integrationType].GetRawNotifierConfig(c.Name)
|
||||
integrationConfig, _ := IntegrationConfigFromType(integrationType)
|
||||
c.Config = integrationConfig
|
||||
|
||||
var settings map[string]any
|
||||
_ = json.Unmarshal(config.Settings, &settings)
|
||||
|
||||
c.Settings = settings
|
||||
|
||||
// Decrypt secure settings over to normal settings.
|
||||
for k, v := range c.SecureSettings {
|
||||
decodeValue, _ := base64.StdEncoding.DecodeString(v)
|
||||
settings[k] = string(decodeValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n IntegrationMutators) WithInvalidConfig(integrationType string) Mutator[Integration] {
|
||||
return func(c *Integration) {
|
||||
integrationConfig, _ := IntegrationConfigFromType(integrationType)
|
||||
c.Config = integrationConfig
|
||||
c.Settings = map[string]interface{}{}
|
||||
c.SecureSettings = map[string]string{}
|
||||
if integrationType == "webex" {
|
||||
// Webex passes validation without any settings but should fail with an unparsable URL.
|
||||
c.Settings["api_url"] = "(*^$*^%!@#$*()"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n IntegrationMutators) WithSettings(settings map[string]any) Mutator[Integration] {
|
||||
return func(c *Integration) {
|
||||
c.Settings = maps.Clone(settings)
|
||||
}
|
||||
}
|
||||
|
||||
func (n IntegrationMutators) AddSetting(key string, val any) Mutator[Integration] {
|
||||
return func(c *Integration) {
|
||||
c.Settings[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
func (n IntegrationMutators) WithSecureSettings(secureSettings map[string]string) Mutator[Integration] {
|
||||
return func(r *Integration) {
|
||||
r.SecureSettings = maps.Clone(secureSettings)
|
||||
}
|
||||
}
|
||||
|
||||
func (n IntegrationMutators) AddSecureSetting(key, val string) Mutator[Integration] {
|
||||
return func(r *Integration) {
|
||||
r.SecureSettings[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
func randomMapKey[K comparable, V any](m map[K]V) (K, V) {
|
||||
randIdx := rand.Intn(len(m))
|
||||
i := 0
|
||||
|
||||
for key, val := range m {
|
||||
if i == randIdx {
|
||||
return key, val
|
||||
}
|
||||
i++
|
||||
}
|
||||
return *new(K), *new(V)
|
||||
}
|
||||
|
||||
func ConvertToRecordingRule(rule *AlertRule) {
|
||||
if rule.Record == nil {
|
||||
rule.Record = &Record{}
|
||||
@@ -1108,3 +1325,7 @@ func ConvertToRecordingRule(rule *AlertRule) {
|
||||
rule.For = 0
|
||||
rule.NotificationSettings = nil
|
||||
}
|
||||
|
||||
func nameToUid(name string) string { // Avoid legacy_storage.NameToUid import cycle.
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(name))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user