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:
Matthew Jacobson
2024-08-26 10:47:53 -04:00
committed by GitHub
parent 22ad1cc16f
commit 32f06c6d9c
36 changed files with 3583 additions and 548 deletions
+433 -3
View File
@@ -1,6 +1,21 @@
package models
import "github.com/grafana/alerting/notify"
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"maps"
"math"
"sort"
"unsafe"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
)
// GetReceiverQuery represents a query for a single receiver.
type GetReceiverQuery struct {
@@ -30,15 +45,430 @@ type ListReceiversQuery struct {
type Receiver struct {
UID string
Name string
Integrations []*notify.GrafanaIntegrationConfig
Integrations []*Integration
Provenance Provenance
Version string
}
func (r *Receiver) Clone() Receiver {
clone := Receiver{
UID: r.UID,
Name: r.Name,
Provenance: r.Provenance,
Version: r.Version,
}
if r.Integrations != nil {
clone.Integrations = make([]*Integration, len(r.Integrations))
for i, integration := range r.Integrations {
cloneIntegration := integration.Clone()
clone.Integrations[i] = &cloneIntegration
}
}
return clone
}
// Encrypt encrypts all integrations.
func (r *Receiver) Encrypt(encryptFn EncryptFn) error {
for _, integration := range r.Integrations {
if err := integration.Encrypt(encryptFn); err != nil {
return err
}
}
return nil
}
// Decrypt decrypts all integrations.
func (r *Receiver) Decrypt(decryptFn DecryptFn) error {
var errs []error
for _, integration := range r.Integrations {
if err := integration.Decrypt(decryptFn); err != nil {
errs = append(errs, fmt.Errorf("failed to decrypt integration %s: %w", integration.UID, err))
}
}
return errors.Join(errs...)
}
// Redact redacts all integrations.
func (r *Receiver) Redact(redactFn RedactFn) {
for _, integration := range r.Integrations {
integration.Redact(redactFn)
}
}
// WithExistingSecureFields copies secure settings from an existing receivers for each integration. Which fields to copy
// is determined by the integrationSecureFields map, which contains a list of secure fields for each integration UID.
func (r *Receiver) WithExistingSecureFields(existing *Receiver, integrationSecureFields map[string][]string) {
existingIntegrations := make(map[string]*Integration, len(existing.Integrations))
for _, integration := range existing.Integrations {
existingIntegrations[integration.UID] = integration
}
for _, integration := range r.Integrations {
if integration.UID == "" {
// This is a new integration, so we don't need to copy any secure fields.
continue
}
fields := integrationSecureFields[integration.UID]
if len(fields) > 0 {
integration.WithExistingSecureFields(existingIntegrations[integration.UID], fields)
}
}
}
// Validate validates all integration settings, ensuring that the integrations are correctly configured.
func (r *Receiver) Validate(decryptFn DecryptFn) error {
var errs []error
for _, integration := range r.Integrations {
if err := integration.Validate(decryptFn); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// Integration is the domain model representation of an integration.
type Integration struct {
UID string
Name string
Config IntegrationConfig
DisableResolveMessage bool
// Settings can contain both secure and non-secure settings either unencrypted or redacted.
Settings map[string]any
// SecureSettings can contain only secure settings either encrypted or redacted.
SecureSettings map[string]string
}
// IntegrationConfig represents the configuration of an integration. It contains the type and information about the fields.
type IntegrationConfig struct {
Type string
Fields map[string]IntegrationField
}
// IntegrationField represents a field in an integration configuration.
type IntegrationField struct {
Name string
Secure bool
}
// IntegrationConfigFromType returns an integration configuration for a given integration type. If the integration type is
// not found an error is returned.
func IntegrationConfigFromType(integrationType string) (IntegrationConfig, error) {
config, err := channels_config.ConfigForIntegrationType(integrationType)
if err != nil {
return IntegrationConfig{}, err
}
integrationConfig := IntegrationConfig{Type: config.Type, Fields: make(map[string]IntegrationField, len(config.Options))}
for _, option := range config.Options {
integrationConfig.Fields[option.PropertyName] = IntegrationField{
Name: option.PropertyName,
Secure: option.Secure,
}
}
return integrationConfig, nil
}
// IsSecureField returns true if the field is both known and marked as secure in the integration configuration.
func (config *IntegrationConfig) IsSecureField(field string) bool {
if config.Fields != nil {
if f, ok := config.Fields[field]; ok {
return f.Secure
}
}
return false
}
func (config *IntegrationConfig) Clone() IntegrationConfig {
clone := IntegrationConfig{
Type: config.Type,
}
if len(config.Fields) > 0 {
clone.Fields = make(map[string]IntegrationField, len(config.Fields))
for key, field := range config.Fields {
clone.Fields[key] = field.Clone()
}
}
return clone
}
func (field *IntegrationField) Clone() IntegrationField {
return IntegrationField{
Name: field.Name,
Secure: field.Secure,
}
}
func (integration *Integration) Clone() Integration {
return Integration{
UID: integration.UID,
Name: integration.Name,
Config: integration.Config.Clone(),
DisableResolveMessage: integration.DisableResolveMessage,
Settings: maps.Clone(integration.Settings),
SecureSettings: maps.Clone(integration.SecureSettings),
}
}
// Encrypt encrypts all fields in Settings that are marked as secure in the integration configuration. The encrypted values
// are stored in SecureSettings and the original values are removed from Settings.
// If a field is already in SecureSettings it is not encrypted again.
func (integration *Integration) Encrypt(encryptFn EncryptFn) error {
var errs []error
for key, val := range integration.Settings {
if isSecureField := integration.Config.IsSecureField(key); !isSecureField {
continue
}
delete(integration.Settings, key)
unencryptedSecureValue, isString := val.(string)
if !isString {
continue
}
if _, exists := integration.SecureSettings[key]; exists {
continue
}
encrypted, err := encryptFn(unencryptedSecureValue)
if err != nil {
errs = append(errs, fmt.Errorf("failed to encrypt secure setting '%s': %w", key, err))
}
integration.SecureSettings[key] = encrypted
}
return errors.Join(errs...)
}
// Decrypt decrypts all fields in SecureSettings and moves them to Settings.
// The original values are removed from SecureSettings.
func (integration *Integration) Decrypt(decryptFn DecryptFn) error {
var errs []error
for key, secureVal := range integration.SecureSettings {
decrypted, err := decryptFn(secureVal)
if err != nil {
errs = append(errs, fmt.Errorf("failed to decrypt secure setting '%s': %w", key, err))
}
delete(integration.SecureSettings, key)
integration.Settings[key] = decrypted
}
return errors.Join(errs...)
}
// Redact redacts all fields in SecureSettings and moves them to Settings.
// The original values are removed from SecureSettings.
func (integration *Integration) Redact(redactFn RedactFn) {
for key, secureVal := range integration.SecureSettings { // TODO: Should we trust that the receiver is stored correctly or use known secure settings?
integration.Settings[key] = redactFn(secureVal)
delete(integration.SecureSettings, key)
}
// We don't trust that the receiver is stored correctly, so we redact secure fields in the settings as well.
for key, val := range integration.Settings {
if val != "" && integration.Config.IsSecureField(key) {
s, isString := val.(string)
if !isString {
continue
}
integration.Settings[key] = redactFn(s)
delete(integration.SecureSettings, key)
}
}
}
// WithExistingSecureFields copies secure settings from an existing integration. Which fields to copy is determined by the
// fields slice.
// Any fields found in Settings or SecureSettings are removed, even if they don't appear in the existing integration.
func (integration *Integration) WithExistingSecureFields(existing *Integration, fields []string) {
// Now for each field marked as secure, we copy the value from the existing receiver.
for _, secureField := range fields {
delete(integration.Settings, secureField) // Ensure secure fields are removed from new settings and secure settings.
delete(integration.SecureSettings, secureField)
if existing != nil {
if existingVal, ok := existing.SecureSettings[secureField]; ok {
integration.SecureSettings[secureField] = existingVal
}
}
}
}
// SecureFields returns a map of all secure fields in the integration. This includes fields in SecureSettings and fields
// in Settings that are marked as secure in the integration configuration.
func (integration *Integration) SecureFields() map[string]bool {
secureFields := make(map[string]bool, len(integration.SecureSettings))
if len(integration.SecureSettings) > 0 {
for key := range integration.SecureSettings {
secureFields[key] = true
}
}
// We mark secure fields in the settings as well. This is to ensure legacy behaviour for redacted secure settings.
for key, val := range integration.Settings {
if val != "" && integration.Config.IsSecureField(key) {
secureFields[key] = true
}
}
return secureFields
}
// Validate validates the integration settings, ensuring that the integration is correctly configured.
func (integration *Integration) Validate(decryptFn DecryptFn) error {
decrypted := integration.Clone()
if err := decrypted.Decrypt(decryptFn); err != nil {
return err
}
jsonBytes, err := json.Marshal(decrypted.Settings)
if err != nil {
return err
}
return ValidateIntegration(context.Background(), alertingNotify.GrafanaIntegrationConfig{
UID: decrypted.UID,
Name: decrypted.Name,
Type: decrypted.Config.Type,
DisableResolveMessage: decrypted.DisableResolveMessage,
Settings: jsonBytes,
SecureSettings: decrypted.SecureSettings,
}, alertingNotify.NoopDecrypt)
}
func ValidateIntegration(ctx context.Context, integration alertingNotify.GrafanaIntegrationConfig, decryptFunc alertingNotify.GetDecryptedValueFn) error {
if integration.Type == "" {
return fmt.Errorf("type should not be an empty string")
}
if integration.Settings == nil {
return fmt.Errorf("settings should not be empty")
}
_, err := alertingNotify.BuildReceiverConfiguration(ctx, &alertingNotify.APIReceiver{
GrafanaIntegrations: alertingNotify.GrafanaIntegrations{
Integrations: []*alertingNotify.GrafanaIntegrationConfig{&integration},
},
}, decryptFunc)
if err != nil {
return err
}
return nil
}
type EncryptFn = func(string) (string, error)
type DecryptFn = func(string) (string, error)
type RedactFn = func(string) string
// Identified describes a class of resources that have a UID. Created to abstract required fields for authorization.
type Identified interface {
GetUID() string
}
func (r Receiver) GetUID() string {
func (r *Receiver) GetUID() string {
return r.UID
}
func (r *Receiver) Fingerprint() string {
sum := fnv.New64()
writeBytes := func(b []byte) {
_, _ = sum.Write(b)
// add a byte sequence that cannot happen in UTF-8 strings.
_, _ = sum.Write([]byte{255})
}
writeString := func(s string) {
if len(s) == 0 {
writeBytes(nil)
return
}
// #nosec G103
// avoid allocation when converting string to byte slice
writeBytes(unsafe.Slice(unsafe.StringData(s), len(s)))
}
// this temp slice is used to convert ints to bytes.
tmp := make([]byte, 8)
writeInt := func(u int) {
binary.LittleEndian.PutUint64(tmp, uint64(u))
writeBytes(tmp)
}
writeIntegration := func(in *Integration) {
writeString(in.UID)
writeString(in.Name)
// Do not include fields in fingerprint as these are not part of the receiver definition.
writeString(in.Config.Type)
if in.DisableResolveMessage {
writeInt(1)
} else {
writeInt(0)
}
// allocate a slice that will be used for sorting keys, so we allocate it only once
var keys []string
maxLen := int(math.Max(float64(len(in.Settings)), float64(len(in.SecureSettings))))
if maxLen > 0 {
keys = make([]string, maxLen)
}
writeSecureSettings := func(secureSettings map[string]string) {
// maps do not guarantee predictable sequence of keys.
// Therefore, to make hash stable, we need to sort keys
if len(secureSettings) == 0 {
return
}
idx := 0
for k := range secureSettings {
keys[idx] = k
idx++
}
sub := keys[:idx]
sort.Strings(sub)
for _, name := range sub {
writeString(name)
writeString(secureSettings[name])
}
}
writeSecureSettings(in.SecureSettings)
writeSettings := func(settings map[string]any) {
// maps do not guarantee predictable sequence of keys.
// Therefore, to make hash stable, we need to sort keys
if len(settings) == 0 {
return
}
idx := 0
for k := range settings {
keys[idx] = k
idx++
}
sub := keys[:idx]
sort.Strings(sub)
for _, name := range sub {
writeString(name)
// TODO: Improve this.
v := settings[name]
bytes, err := json.Marshal(v)
if err != nil {
writeString(fmt.Sprintf("%+v", v))
} else {
writeBytes(bytes)
}
}
}
writeSettings(in.Settings)
}
// fields that determine the rule state
writeString(r.UID)
writeString(r.Name)
writeString(string(r.Provenance))
for _, integration := range r.Integrations {
writeIntegration(integration)
}
return fmt.Sprintf("%016x", sum.Sum64())
}
@@ -0,0 +1,399 @@
package models
import (
"reflect"
"testing"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
)
func TestReceiver_Clone(t *testing.T) {
testCases := []struct {
name string
receiver Receiver
}{
{name: "empty receiver", receiver: Receiver{}},
{name: "empty integration", receiver: Receiver{Integrations: []*Integration{{Config: IntegrationConfig{}}}}},
{name: "random receiver", receiver: ReceiverGen()()},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
receiverClone := tc.receiver.Clone()
assert.Equal(t, tc.receiver, receiverClone)
for _, integration := range tc.receiver.Integrations {
integrationClone := integration.Clone()
assert.Equal(t, *integration, integrationClone)
}
})
}
}
func TestReceiver_EncryptDecrypt(t *testing.T) {
encryptFn := Base64Enrypt
decryptnFn := Base64Decrypt
// Test that all known integration types encrypt and decrypt their secrets.
for integrationType := range alertingNotify.AllKnownConfigsForTesting {
t.Run(integrationType, func(t *testing.T) {
decrypedIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
encrypted := decrypedIntegration.Clone()
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType)
assert.NoError(t, err)
for _, key := range secrets {
if val, ok := encrypted.Settings[key]; ok {
if s, isString := val.(string); isString {
encryptedVal, err := encryptFn(s)
assert.NoError(t, err)
encrypted.SecureSettings[key] = encryptedVal
delete(encrypted.Settings, key)
}
}
}
testIntegration := decrypedIntegration.Clone()
err = testIntegration.Encrypt(encryptFn)
assert.NoError(t, err)
assert.Equal(t, encrypted, testIntegration)
err = testIntegration.Decrypt(decryptnFn)
assert.NoError(t, err)
assert.Equal(t, decrypedIntegration, testIntegration)
})
}
}
func TestIntegration_Redact(t *testing.T) {
redactFn := func(key string) string {
return "TESTREDACTED"
}
// Test that all known integration types redact their secrets.
for integrationType := range alertingNotify.AllKnownConfigsForTesting {
t.Run(integrationType, func(t *testing.T) {
validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
expected := validIntegration.Clone()
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType)
assert.NoError(t, err)
for _, key := range secrets {
if val, ok := expected.Settings[key]; ok {
if s, isString := val.(string); isString && s != "" {
expected.Settings[key] = redactFn(s)
delete(expected.SecureSettings, key)
}
}
}
validIntegration.Redact(redactFn)
assert.Equal(t, expected, validIntegration)
})
}
}
func TestIntegration_Validate(t *testing.T) {
// Test that all known integration types are valid.
for integrationType := range alertingNotify.AllKnownConfigsForTesting {
t.Run(integrationType, func(t *testing.T) {
validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
assert.NoError(t, validIntegration.Encrypt(Base64Enrypt))
assert.NoErrorf(t, validIntegration.Validate(Base64Decrypt), "integration should be valid")
invalidIntegration := IntegrationGen(IntegrationMuts.WithInvalidConfig(integrationType))()
assert.NoError(t, invalidIntegration.Encrypt(Base64Enrypt))
assert.Errorf(t, invalidIntegration.Validate(Base64Decrypt), "integration should be invalid")
})
}
}
func TestIntegration_WithExistingSecureFields(t *testing.T) {
// Test that WithExistingSecureFields will copy over the secure fields from the existing integration.
testCases := []struct {
name string
integration Integration
secureFields []string
existing Integration
expected Integration
}{
{
name: "test receiver",
integration: Integration{
SecureSettings: map[string]string{
"f1": "newVal1",
"f2": "newVal2",
"f3": "newVal3",
"f5": "newVal5",
},
},
secureFields: []string{"f2", "f4", "f5"},
existing: Integration{
SecureSettings: map[string]string{
"f1": "oldVal1",
"f2": "oldVal2",
"f3": "oldVal3",
"f4": "oldVal4",
},
},
expected: Integration{
SecureSettings: map[string]string{
"f1": "newVal1",
"f2": "oldVal2",
"f3": "newVal3",
"f4": "oldVal4",
},
},
},
{
name: "Integration[exists], SecureFields[true], Existing[exists]: old value",
integration: Integration{
SecureSettings: map[string]string{"f1": "newVal1"},
},
secureFields: []string{"f1"},
existing: Integration{SecureSettings: map[string]string{"f1": "oldVal1"}},
expected: Integration{SecureSettings: map[string]string{"f1": "oldVal1"}},
},
{
name: "Integration[exists], SecureFields[true], Existing[missing]: no value",
integration: Integration{
SecureSettings: map[string]string{"f1": "newVal1"},
},
secureFields: []string{"f1"},
existing: Integration{SecureSettings: map[string]string{}},
expected: Integration{SecureSettings: map[string]string{}},
},
{
name: "Integration[exists], SecureFields[false], Existing[exists]: new value",
integration: Integration{
SecureSettings: map[string]string{"f1": "newVal1"},
},
existing: Integration{SecureSettings: map[string]string{"f1": "oldVal1"}},
expected: Integration{SecureSettings: map[string]string{"f1": "newVal1"}},
},
{
name: "Integration[exists], SecureFields[false], Existing[missing]: new value",
integration: Integration{
SecureSettings: map[string]string{"f1": "newVal1"},
},
existing: Integration{SecureSettings: map[string]string{}},
expected: Integration{SecureSettings: map[string]string{"f1": "newVal1"}},
},
{
name: "Integration[missing], SecureFields[true], Existing[exists]: old value",
integration: Integration{
SecureSettings: map[string]string{},
},
secureFields: []string{"f1"},
existing: Integration{SecureSettings: map[string]string{"f1": "oldVal1"}},
expected: Integration{SecureSettings: map[string]string{"f1": "oldVal1"}},
},
{
name: "Integration[missing], SecureFields[true], Existing[missing]: no value",
integration: Integration{
SecureSettings: map[string]string{},
},
secureFields: []string{"f1"},
existing: Integration{SecureSettings: map[string]string{}},
expected: Integration{SecureSettings: map[string]string{}},
},
{
name: "Integration[missing], SecureFields[false], Existing[exists]: no value",
integration: Integration{
SecureSettings: map[string]string{},
},
existing: Integration{SecureSettings: map[string]string{"f1": "oldVal1"}},
expected: Integration{SecureSettings: map[string]string{}},
},
{
name: "Integration[missing], SecureFields[false], Existing[missing]: no value",
integration: Integration{
SecureSettings: map[string]string{},
},
existing: Integration{SecureSettings: map[string]string{}},
expected: Integration{SecureSettings: map[string]string{}},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.integration.WithExistingSecureFields(&tc.existing, tc.secureFields)
assert.Equal(t, tc.expected, tc.integration)
})
}
}
func TestIntegrationConfig(t *testing.T) {
// Test that all known integration types have a config and correctly mark their secrets as secure.
for integrationType := range alertingNotify.AllKnownConfigsForTesting {
t.Run(integrationType, func(t *testing.T) {
config, err := IntegrationConfigFromType(integrationType)
assert.NoError(t, err)
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType)
assert.NoError(t, err)
allSecrets := make(map[string]struct{}, len(secrets))
for _, key := range secrets {
allSecrets[key] = struct{}{}
}
for field := range config.Fields {
_, isSecret := allSecrets[field]
assert.Equal(t, isSecret, config.IsSecureField(field))
}
assert.False(t, config.IsSecureField("__--**unknown_field**--__"))
})
}
t.Run("Unknown type returns error", func(t *testing.T) {
_, err := IntegrationConfigFromType("__--**unknown_type**--__")
assert.Error(t, err)
})
}
func TestIntegration_SecureFields(t *testing.T) {
// Test that all known integration types have a config and correctly mark their secrets as secure.
for integrationType := range alertingNotify.AllKnownConfigsForTesting {
t.Run(integrationType, func(t *testing.T) {
t.Run("contains SecureSettings", func(t *testing.T) {
validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
expected := make(map[string]bool, len(validIntegration.SecureSettings))
for field := range validIntegration.Config.Fields {
if validIntegration.Config.IsSecureField(field) {
expected[field] = true
validIntegration.SecureSettings[field] = "test"
delete(validIntegration.Settings, field)
}
}
assert.Equal(t, expected, validIntegration.SecureFields())
})
t.Run("contains secret Settings not in SecureSettings", func(t *testing.T) {
validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
expected := make(map[string]bool, len(validIntegration.SecureSettings))
for field := range validIntegration.Config.Fields {
if validIntegration.Config.IsSecureField(field) {
expected[field] = true
validIntegration.Settings[field] = "test"
delete(validIntegration.SecureSettings, field)
}
}
assert.Equal(t, expected, validIntegration.SecureFields())
})
})
}
}
// This is a broken type that will error if marshalled.
type broken struct {
f1 string
}
func (b broken) MarshalJSON() ([]byte, error) {
return nil, assert.AnError
}
func TestReceiver_Fingerprint(t *testing.T) {
// Test that the fingerprint is stable.
im := IntegrationMuts
baseReceiver := ReceiverGen(ReceiverMuts.WithName("test receiver"), ReceiverMuts.WithIntegrations(
IntegrationGen(im.WithName("test receiver"), im.WithValidConfig("slack"))(),
))()
baseReceiver.Integrations[0].UID = "stable UID"
baseReceiver.Integrations[0].DisableResolveMessage = true
baseReceiver.Integrations[0].SecureSettings = map[string]string{"test2": "test2"}
baseReceiver.Integrations[0].Settings["broken"] = broken{f1: "this"} // Add a broken type to ensure it is stable in the fingerprint.
baseReceiver.Integrations[0].Config = IntegrationConfig{Type: baseReceiver.Integrations[0].Config.Type} // Remove all fields except Type.
completelyDifferentReceiver := ReceiverGen(ReceiverMuts.WithName("test receiver2"), ReceiverMuts.WithIntegrations(
IntegrationGen(im.WithName("test receiver2"), im.WithValidConfig("discord"))(),
))()
completelyDifferentReceiver.Integrations[0].UID = "stable UID2"
completelyDifferentReceiver.Integrations[0].DisableResolveMessage = false
completelyDifferentReceiver.Integrations[0].SecureSettings = map[string]string{"test": "test"}
completelyDifferentReceiver.Provenance = ProvenanceAPI
completelyDifferentReceiver.Integrations[0].Config = IntegrationConfig{Type: completelyDifferentReceiver.Integrations[0].Config.Type} // Remove all fields except Type.
t.Run("stable across code changes", func(t *testing.T) {
expectedFingerprint := "ae141b582965f4f5" // If this is a valid fingerprint generation change, update the expected value.
assert.Equal(t, expectedFingerprint, baseReceiver.Fingerprint())
})
t.Run("stable across clones", func(t *testing.T) {
fingerprint := baseReceiver.Fingerprint()
receiverClone := baseReceiver.Clone()
assert.Equal(t, fingerprint, receiverClone.Fingerprint())
})
t.Run("stable across Version field modification", func(t *testing.T) {
fingerprint := baseReceiver.Fingerprint()
receiverClone := baseReceiver.Clone()
receiverClone.Version = "new version"
assert.Equal(t, fingerprint, receiverClone.Fingerprint())
})
t.Run("unstable across field modification", func(t *testing.T) {
fingerprint := baseReceiver.Fingerprint()
excludedFields := map[string]struct{}{
"Version": {},
}
reflectVal := reflect.ValueOf(&completelyDifferentReceiver).Elem()
receiverType := reflect.TypeOf((*Receiver)(nil)).Elem()
for i := 0; i < receiverType.NumField(); i++ {
field := receiverType.Field(i).Name
if _, ok := excludedFields[field]; ok {
continue
}
cp := baseReceiver.Clone()
// Get the current field being modified.
v := reflect.ValueOf(&cp).Elem()
vf := v.Field(i)
otherField := reflectVal.Field(i)
if reflect.DeepEqual(otherField.Interface(), vf.Interface()) {
assert.Failf(t, "filds are identical", "Receiver field %s is the same as the original, test does not ensure instability across the field", field)
continue
}
// Set the field to the value of the completelyDifferentReceiver.
vf.Set(otherField)
f2 := cp.Fingerprint()
assert.NotEqualf(t, fingerprint, f2, "Receiver field %s does not seem to be used in fingerprint", field)
}
excludedFields = map[string]struct{}{}
reflectVal = reflect.ValueOf(completelyDifferentReceiver.Integrations[0]).Elem()
integrationType := reflect.TypeOf((*Integration)(nil)).Elem()
for i := 0; i < integrationType.NumField(); i++ {
field := integrationType.Field(i).Name
if _, ok := excludedFields[field]; ok {
continue
}
cp := baseReceiver.Clone()
integrationCp := cp.Integrations[0]
// Get the current field being modified.
v := reflect.ValueOf(integrationCp).Elem()
vf := v.Field(i)
otherField := reflectVal.Field(i)
if reflect.DeepEqual(otherField.Interface(), vf.Interface()) {
assert.Failf(t, "filds are identical", "Integration field %s is the same as the original, test does not ensure instability across the field", field)
continue
}
// Set the field to the value of the completelyDifferentReceiver.
vf.Set(otherField)
f2 := cp.Fingerprint()
assert.NotEqualf(t, fingerprint, f2, "Integration field %s does not seem to be used in fingerprint", field)
}
})
}
+221
View File
@@ -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))
}