Alerting: v0 schema for integrations (mimir) (#110908)

* generate schema for mimir integrations from schema on front-end
* review and fix the settings
* Update GetAvailableNotifiersV2 to return mimir as v0
* add version argument to GetSecretKeysForContactPointType
* update TestGetSecretKeysForContactPointType to include v0
* add type alias field to contain alternate types that different from Grafana's
* add support for msteamsv2
* update ConfigForIntegrationType to look for alternate type
* update IntegrationConfigFromType to use new result of ConfigForIntegrationType
* add reference to parent plugin to NotifierPluginVersion to allow getting plugin type by it's alias
* add tests to ensure consistency
* make API response stable
* add tests against snapshot + omit optional fields
This commit is contained in:
Yuri Tseretyan 2025-09-17 09:25:56 -04:00 committed by GitHub
parent a6db37c2b7
commit c36b2ae191
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 17112 additions and 110 deletions

View File

@ -3,6 +3,7 @@ package api
import (
"net/http"
"slices"
"strings"
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
@ -12,7 +13,10 @@ import (
func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) response.Response {
return func(r *contextmodel.ReqContext) response.Response {
if r.Query("version") == "2" {
return response.JSON(http.StatusOK, slices.Collect(channels_config.GetAvailableNotifiersV2()))
v2 := slices.SortedFunc(channels_config.GetAvailableNotifiersV2(), func(a, b *channels_config.VersionedNotifierPlugin) int {
return strings.Compare(a.Type, b.Type)
})
return response.JSON(http.StatusOK, v2)
}
return response.JSON(http.StatusOK, channels_config.GetAvailableNotifiers())
}

View File

@ -228,23 +228,21 @@ func (f IntegrationFieldPath) With(segment string) IntegrationFieldPath {
// IntegrationConfig - The integration configuration
// error - Error if integration type not found or invalid version specified
func IntegrationConfigFromType(integrationType string, version *string) (IntegrationConfig, error) {
config, err := channels_config.ConfigForIntegrationType(integrationType)
versionConfig, err := channels_config.ConfigForIntegrationType(integrationType)
if err != nil {
return IntegrationConfig{}, err
}
var versionConfig channels_config.NotifierPluginVersion
if version == nil {
versionConfig = config.GetCurrentVersion()
} else {
var ok bool
versionConfig, ok = config.GetVersion(*version)
if !ok {
// if particular version is requested and the version returned does not match, try to get the correct version
if version != nil && *version != string(versionConfig.Version) {
exists := false
versionConfig, exists = versionConfig.Plugin.GetVersion(channels_config.NotifierVersion(*version))
if !exists {
return IntegrationConfig{}, fmt.Errorf("version %s not found in config", *version)
}
}
integrationConfig := IntegrationConfig{
Type: config.Type,
Version: versionConfig.Version,
Type: versionConfig.Plugin.Type,
Version: string(versionConfig.Version),
Fields: make(map[string]IntegrationField, len(versionConfig.Options)),
}
for _, option := range versionConfig.Options {

View File

@ -46,7 +46,7 @@ func TestReceiver_EncryptDecrypt(t *testing.T) {
decrypedIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
encrypted := decrypedIntegration.Clone()
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType)
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1)
assert.NoError(t, err)
for _, key := range secrets {
val, ok, err := extractField(encrypted.Settings, NewIntegrationFieldPath(key))
@ -82,7 +82,7 @@ func TestIntegration_Redact(t *testing.T) {
validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
expected := validIntegration.Clone()
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType)
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1)
assert.NoError(t, err)
for _, key := range secrets {
err := setField(expected.Settings, NewIntegrationFieldPath(key), func(current any) any {
@ -247,12 +247,12 @@ func TestSecretsIntegrationConfig(t *testing.T) {
assert.NoError(t, err)
t.Run("v1 is current", func(t *testing.T) {
configv1, err := IntegrationConfigFromType(integrationType, util.Pointer("v1"))
configv1, err := IntegrationConfigFromType(integrationType, util.Pointer(string(channels_config.V1)))
assert.NoError(t, err)
assert.Equal(t, config, configv1)
})
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType)
secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1)
assert.NoError(t, err)
allSecrets := make(map[string]struct{}, len(secrets))
for _, key := range secrets {

View File

@ -4,7 +4,6 @@ import (
"fmt"
"iter"
"maps"
"os"
"strings"
"github.com/grafana/alerting/receivers/jira"
@ -14,10 +13,20 @@ import (
alertingTemplates "github.com/grafana/alerting/templates"
)
type NotifierVersion string
const (
// versions that contain the "mimir" tag in their name are dedicated to integrations supported by Mimir.
// By default, all mimir integrations should use the V0mimir1 version.
// Exceptions are Mimir integrations that have multiple configurations for the same Grafana type.
V0mimir1 NotifierVersion = "v0mimir1"
V0mimir2 NotifierVersion = "v0mimir2"
V1 NotifierVersion = "v1"
)
// GetAvailableNotifiers returns the metadata of all the notification channels that can be configured.
func GetAvailableNotifiers() []*NotifierPlugin {
hostname, _ := os.Hostname()
pushoverSoundOptions := []SelectOption{
{
Value: "default",
@ -500,7 +509,7 @@ func GetAvailableNotifiers() []*NotifierPlugin {
Description: "The unique location of the affected system, preferably a hostname or FQDN. You can use templates",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: hostname,
Placeholder: "grafana.local",
PropertyName: "source",
},
{ // New in 9.4.
@ -2055,14 +2064,23 @@ func GetAvailableNotifiers() []*NotifierPlugin {
}
// GetSecretKeysForContactPointType returns settings keys of contact point of the given type that are expected to be secrets. Returns error is contact point type is not known.
func GetSecretKeysForContactPointType(contactPointType string) ([]string, error) {
notifiers := GetAvailableNotifiers()
func GetSecretKeysForContactPointType(contactPointType string, version NotifierVersion) ([]string, error) {
var notifiers []*NotifierPlugin
if version == V1 {
notifiers = GetAvailableNotifiers()
}
if version == V0mimir1 {
notifiers = getAvailableV0mimir1Notifiers()
}
if version == V0mimir2 {
notifiers = getAvailableV0mimir2Notifiers()
}
for _, n := range notifiers {
if strings.EqualFold(n.Type, contactPointType) {
return getSecretFields("", n.Options), nil
}
}
return nil, fmt.Errorf("no secrets configured for type '%s'", contactPointType)
return nil, fmt.Errorf("no secrets configured for type '%s' of version %s", contactPointType, version)
}
func getSecretFields(parentPath string, options []NotifierOption) []string {
@ -2084,37 +2102,53 @@ func getSecretFields(parentPath string, options []NotifierOption) []string {
}
// ConfigForIntegrationType returns the config for the given integration type. Returns error is integration type is not known.
func ConfigForIntegrationType(contactPointType string) (VersionedNotifierPlugin, error) {
func ConfigForIntegrationType(contactPointTypeOrAlternateType string) (NotifierPluginVersion, error) {
notifiers := GetAvailableNotifiersV2()
for n := range notifiers {
if strings.EqualFold(n.Type, contactPointType) {
return n, nil
if strings.EqualFold(n.Type, contactPointTypeOrAlternateType) {
return n.GetCurrentVersion(), nil
}
for _, version := range n.Versions {
if version.TypeAlias == "" {
continue
}
if strings.EqualFold(version.TypeAlias, contactPointTypeOrAlternateType) {
return version, nil
}
}
}
return VersionedNotifierPlugin{}, fmt.Errorf("unknown integration type '%s'", contactPointType)
return NotifierPluginVersion{}, fmt.Errorf("unknown integration type '%s'", contactPointTypeOrAlternateType)
}
func GetAvailableNotifiersV2() iter.Seq[VersionedNotifierPlugin] {
v1 := GetAvailableNotifiers()
m := make(map[string]VersionedNotifierPlugin, len(v1))
for _, n := range v1 {
pl := VersionedNotifierPlugin{
Type: n.Type,
Name: n.Name,
Description: n.Description,
Heading: n.Heading,
Info: n.Info,
CurrentVersion: "v1",
Versions: []NotifierPluginVersion{
{
Version: "v1",
CanCreate: true,
Options: n.Options,
Info: "",
},
},
func GetAvailableNotifiersV2() iter.Seq[*VersionedNotifierPlugin] {
m := make(map[string]*VersionedNotifierPlugin, 24) // we support 24 notifier types
add := func(n []*NotifierPlugin, version NotifierVersion) {
for _, n := range n {
pl, ok := m[n.Type]
if !ok {
pl = &VersionedNotifierPlugin{
Type: n.Type,
Name: n.Name,
Description: n.Description,
Heading: n.Heading,
Info: n.Info,
CurrentVersion: version,
Versions: make([]NotifierPluginVersion, 0, 2), // usually, there are 2 versions per type
}
m[n.Type] = pl
}
pl.Versions = append(pl.Versions, NotifierPluginVersion{
Version: version,
// we allow users to create only v1 notifiers
CanCreate: version == V1,
Options: n.Options,
TypeAlias: n.TypeAlias,
Plugin: pl,
})
}
m[n.Type] = pl
}
add(GetAvailableNotifiers(), V1)
add(getAvailableV0mimir2Notifiers(), V0mimir2)
add(getAvailableV0mimir1Notifiers(), V0mimir1)
return maps.Values(m)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,38 @@
package channels_config
import (
"encoding/json"
"fmt"
"maps"
"reflect"
"slices"
"strings"
"testing"
"github.com/grafana/alerting/notify/notifytest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetSecretKeysForContactPointType(t *testing.T) {
httpConfigSecrets := []string{"http_config.authorization.credentials", "http_config.basic_auth.password", "http_config.oauth2.client_secret"}
testCases := []struct {
receiverType string
version NotifierVersion
expectedSecretFields []string
}{
{receiverType: "dingding", expectedSecretFields: []string{"url"}},
{receiverType: "kafka", expectedSecretFields: []string{"password"}},
{receiverType: "email", expectedSecretFields: []string{}},
{receiverType: "pagerduty", expectedSecretFields: []string{"integrationKey"}},
{receiverType: "victorops", expectedSecretFields: []string{"url"}},
{receiverType: "oncall", expectedSecretFields: []string{"password", "authorization_credentials"}},
{receiverType: "pushover", expectedSecretFields: []string{"apiToken", "userKey"}},
{receiverType: "slack", expectedSecretFields: []string{"token", "url"}},
{receiverType: "sensugo", expectedSecretFields: []string{"apikey"}},
{receiverType: "teams", expectedSecretFields: []string{}},
{receiverType: "telegram", expectedSecretFields: []string{"bottoken"}},
{receiverType: "webhook", expectedSecretFields: []string{
{receiverType: "dingding", version: V1, expectedSecretFields: []string{"url"}},
{receiverType: "kafka", version: V1, expectedSecretFields: []string{"password"}},
{receiverType: "email", version: V1, expectedSecretFields: []string{}},
{receiverType: "pagerduty", version: V1, expectedSecretFields: []string{"integrationKey"}},
{receiverType: "victorops", version: V1, expectedSecretFields: []string{"url"}},
{receiverType: "oncall", version: V1, expectedSecretFields: []string{"password", "authorization_credentials"}},
{receiverType: "pushover", version: V1, expectedSecretFields: []string{"apiToken", "userKey"}},
{receiverType: "slack", version: V1, expectedSecretFields: []string{"token", "url"}},
{receiverType: "sensugo", version: V1, expectedSecretFields: []string{"apikey"}},
{receiverType: "teams", version: V1, expectedSecretFields: []string{}},
{receiverType: "telegram", version: V1, expectedSecretFields: []string{"bottoken"}},
{receiverType: "webhook", version: V1, expectedSecretFields: []string{
"password",
"authorization_credentials",
"tlsConfig.caCertificate",
@ -34,44 +44,147 @@ func TestGetSecretKeysForContactPointType(t *testing.T) {
"http_config.oauth2.tls_config.clientCertificate",
"http_config.oauth2.tls_config.clientKey",
}},
{receiverType: "wecom", expectedSecretFields: []string{"url", "secret"}},
{receiverType: "prometheus-alertmanager", expectedSecretFields: []string{"basicAuthPassword"}},
{receiverType: "discord", expectedSecretFields: []string{"url"}},
{receiverType: "googlechat", expectedSecretFields: []string{"url"}},
{receiverType: "LINE", expectedSecretFields: []string{"token"}},
{receiverType: "threema", expectedSecretFields: []string{"api_secret"}},
{receiverType: "opsgenie", expectedSecretFields: []string{"apiKey"}},
{receiverType: "webex", expectedSecretFields: []string{"bot_token"}},
{receiverType: "sns", expectedSecretFields: []string{"sigv4.access_key", "sigv4.secret_key"}},
{receiverType: "mqtt", expectedSecretFields: []string{"password", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey"}},
{receiverType: "jira", expectedSecretFields: []string{"user", "password", "api_token"}},
{receiverType: "wecom", version: V1, expectedSecretFields: []string{"url", "secret"}},
{receiverType: "prometheus-alertmanager", version: V1, expectedSecretFields: []string{"basicAuthPassword"}},
{receiverType: "discord", version: V1, expectedSecretFields: []string{"url"}},
{receiverType: "googlechat", version: V1, expectedSecretFields: []string{"url"}},
{receiverType: "LINE", version: V1, expectedSecretFields: []string{"token"}},
{receiverType: "threema", version: V1, expectedSecretFields: []string{"api_secret"}},
{receiverType: "opsgenie", version: V1, expectedSecretFields: []string{"apiKey"}},
{receiverType: "webex", version: V1, expectedSecretFields: []string{"bot_token"}},
{receiverType: "sns", version: V1, expectedSecretFields: []string{"sigv4.access_key", "sigv4.secret_key"}},
{receiverType: "mqtt", version: V1, expectedSecretFields: []string{"password", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey"}},
{receiverType: "jira", version: V1, expectedSecretFields: []string{"user", "password", "api_token"}},
{receiverType: "victorops", version: V0mimir1, expectedSecretFields: append([]string{"api_key"}, httpConfigSecrets...)},
{receiverType: "sns", version: V0mimir1, expectedSecretFields: append([]string{"sigv4.SecretKey"}, httpConfigSecrets...)},
{receiverType: "telegram", version: V0mimir1, expectedSecretFields: append([]string{"token"}, httpConfigSecrets...)},
{receiverType: "discord", version: V0mimir1, expectedSecretFields: append([]string{"webhook_url"}, httpConfigSecrets...)},
{receiverType: "pagerduty", version: V0mimir1, expectedSecretFields: append([]string{"routing_key", "service_key"}, httpConfigSecrets...)},
{receiverType: "pushover", version: V0mimir1, expectedSecretFields: append([]string{"user_key", "token"}, httpConfigSecrets...)},
{receiverType: "jira", version: V0mimir1, expectedSecretFields: httpConfigSecrets},
{receiverType: "opsgenie", version: V0mimir1, expectedSecretFields: append([]string{"api_key"}, httpConfigSecrets...)},
{receiverType: "teams", version: V0mimir1, expectedSecretFields: append([]string{"webhook_url"}, httpConfigSecrets...)},
{receiverType: "teams", version: V0mimir2, expectedSecretFields: append([]string{"webhook_url"}, httpConfigSecrets...)},
{receiverType: "email", version: V0mimir1, expectedSecretFields: []string{"auth_password", "auth_secret"}},
{receiverType: "slack", version: V0mimir1, expectedSecretFields: append([]string{"api_url"}, httpConfigSecrets...)},
{receiverType: "webex", version: V0mimir1, expectedSecretFields: httpConfigSecrets},
{receiverType: "wechat", version: V0mimir1, expectedSecretFields: append([]string{"api_secret"}, httpConfigSecrets...)},
{receiverType: "webhook", version: V0mimir1, expectedSecretFields: append([]string{"url"}, httpConfigSecrets...)},
}
n := GetAvailableNotifiers()
allTypes := make(map[string]struct{}, len(n))
for _, plugin := range n {
allTypes[plugin.Type] = struct{}{}
n := slices.Collect(GetAvailableNotifiersV2())
type typeWithVersion struct {
Type string
Version NotifierVersion
}
allTypes := make(map[typeWithVersion]struct{}, len(n))
getKey := func(pluginType string, version NotifierVersion) typeWithVersion {
return typeWithVersion{pluginType, version}
}
for _, p := range n {
for _, v := range p.Versions {
allTypes[getKey(p.Type, v.Version)] = struct{}{}
}
}
for _, testCase := range testCases {
delete(allTypes, testCase.receiverType)
t.Run(testCase.receiverType, func(t *testing.T) {
got, err := GetSecretKeysForContactPointType(testCase.receiverType)
delete(allTypes, getKey(testCase.receiverType, testCase.version))
t.Run(fmt.Sprintf("%s-%s", testCase.receiverType, testCase.version), func(t *testing.T) {
got, err := GetSecretKeysForContactPointType(testCase.receiverType, testCase.version)
require.NoError(t, err)
require.ElementsMatch(t, testCase.expectedSecretFields, got)
})
}
for integrationType := range allTypes {
t.Run(integrationType, func(t *testing.T) {
got, err := GetSecretKeysForContactPointType(integrationType)
for it := range allTypes {
t.Run(fmt.Sprintf("%s-%s", it.Type, it.Version), func(t *testing.T) {
got, err := GetSecretKeysForContactPointType(it.Type, it.Version)
require.NoError(t, err)
require.Emptyf(t, got, "secret keys for %s should be empty", integrationType)
require.Emptyf(t, got, "secret keys for version %s of %s should be empty", it.Version, it.Type)
})
}
require.Emptyf(t, allTypes, "not all types are covered: %s", allTypes)
}
func TestGetAvailableNotifiersV2(t *testing.T) {
n := slices.Collect(GetAvailableNotifiersV2())
require.NotEmpty(t, n)
for _, notifier := range n {
t.Run(fmt.Sprintf("integration %s [%s]", notifier.Type, notifier.Name), func(t *testing.T) {
currentVersion := V1
if notifier.Type == "wechat" {
currentVersion = V0mimir1
}
t.Run(fmt.Sprintf("current version is %s", currentVersion), func(t *testing.T) {
require.Equal(t, currentVersion, notifier.GetCurrentVersion().Version)
})
t.Run("should be able to create only v1", func(t *testing.T) {
for _, version := range notifier.Versions {
if version.Version == V1 {
require.True(t, version.CanCreate, "v1 should be able to create")
continue
}
require.False(t, version.CanCreate, "v0 should not be able to create")
}
})
})
}
}
func TestConfigForIntegrationType(t *testing.T) {
t.Run("should return current version for all common types", func(t *testing.T) {
for plugin := range GetAvailableNotifiersV2() {
t.Run(plugin.Type, func(t *testing.T) {
version, err := ConfigForIntegrationType(plugin.Type)
require.NoErrorf(t, err, "expected config but got error for plugin type %s", plugin.Type)
assert.Equal(t, version.Plugin, plugin)
assert.Equal(t, version, plugin.GetCurrentVersion())
})
}
})
t.Run("should return specific version if matched by alias", func(t *testing.T) {
for plugin := range GetAvailableNotifiersV2() {
for _, version := range plugin.Versions {
if version.TypeAlias == "" {
continue
}
t.Run(version.TypeAlias, func(t *testing.T) {
actualVersion, err := ConfigForIntegrationType(version.TypeAlias)
require.NoErrorf(t, err, "expected config but got error for plugin type %s", plugin.Type)
assert.Equal(t, version, actualVersion)
})
}
}
})
t.Run("should return error if not known type", func(t *testing.T) {
_, err := ConfigForIntegrationType("unknown")
require.Error(t, err)
})
}
func TestTypeUniqueness(t *testing.T) {
knownTypes := make(map[string]struct{})
for plugin := range GetAvailableNotifiersV2() {
iType := strings.ToLower(plugin.Type)
if _, ok := knownTypes[iType]; ok {
assert.Failf(t, "duplicate plugin type", "plugin type %s", plugin.Type)
}
knownTypes[iType] = struct{}{}
for _, version := range plugin.Versions {
if version.TypeAlias == "" {
continue
}
iType = strings.ToLower(version.TypeAlias)
if _, ok := knownTypes[iType]; ok {
assert.Failf(t, "mimir type duplicates Grafana plugin type", "plugin type %s", iType)
}
knownTypes[iType] = struct{}{}
}
}
}
func Test_getSecretFields(t *testing.T) {
testCases := []struct {
name string
@ -116,3 +229,62 @@ func Test_getSecretFields(t *testing.T) {
})
}
}
func TestV0IntegrationsSecrets(t *testing.T) {
// This test ensures that all known integrations' secrets are listed in the schema definition.
notifytest.ForEachIntegrationType(t, func(configType reflect.Type) {
t.Run(configType.Name(), func(t *testing.T) {
integrationType := strings.ToLower(strings.TrimSuffix(configType.Name(), "Config"))
pluginVersion, err := ConfigForIntegrationType(integrationType)
require.NoError(t, err)
if pluginVersion.Version == V1 {
var ok bool
pluginVersion, ok = pluginVersion.Plugin.GetVersion(V0mimir1)
require.True(t, ok)
}
expectedSecrets := pluginVersion.GetSecretFieldsPaths()
var secrets []string
for option := range maps.Keys(notifytest.ValidMimirHTTPConfigs) {
cfg, err := notifytest.GetMimirIntegrationForType(configType, option)
require.NoError(t, err)
data, err := json.Marshal(cfg)
require.NoError(t, err)
m := map[string]any{}
err = json.Unmarshal(data, &m)
require.NoError(t, err)
secrets = append(secrets, getSecrets(m, "")...)
}
secrets = unique(secrets)
t.Log(secrets)
require.ElementsMatch(t, expectedSecrets, secrets)
})
})
}
func unique(slice []string) []string {
keys := make(map[string]struct{}, len(slice))
list := make([]string, 0, len(slice))
for _, entry := range slice {
if _, value := keys[entry]; !value {
keys[entry] = struct{}{}
list = append(list, entry)
}
}
return list
}
func getSecrets(m map[string]any, parent string) []string {
var result []string
for key, val := range m {
str, ok := val.(string)
if ok && str == "<secret>" {
result = append(result, parent+key)
}
m, ok := val.(map[string]any)
if ok {
subSecrets := getSecrets(m, parent+key+".")
result = append(result, subSecrets...)
}
}
return result
}

View File

@ -3,6 +3,7 @@ package channels_config
// NotifierPlugin holds meta information about a notifier.
type NotifierPlugin struct {
Type string `json:"type"`
TypeAlias string `json:"typeAlias,omitempty"`
Name string `json:"name"`
Heading string `json:"heading"`
Description string `json:"description"`
@ -14,16 +15,16 @@ type NotifierPlugin struct {
// It includes metadata such as type, name, description, and version-specific details.
type VersionedNotifierPlugin struct {
Type string `json:"type"`
CurrentVersion string `json:"currentVersion"`
CurrentVersion NotifierVersion `json:"currentVersion"`
Name string `json:"name"`
Heading string `json:"heading"`
Description string `json:"description"`
Info string `json:"info"`
Heading string `json:"heading,omitempty"`
Description string `json:"description,omitempty"`
Info string `json:"info,omitempty"`
Versions []NotifierPluginVersion `json:"versions"`
}
// GetVersion retrieves a specific version of the notifier plugin by its version string. Returns the version and a boolean indicating success.
func (p VersionedNotifierPlugin) GetVersion(v string) (NotifierPluginVersion, bool) {
func (p VersionedNotifierPlugin) GetVersion(v NotifierVersion) (NotifierPluginVersion, bool) {
for _, version := range p.Versions {
if version.Version == v {
return version, true
@ -44,10 +45,17 @@ func (p VersionedNotifierPlugin) GetCurrentVersion() NotifierPluginVersion {
// NotifierPluginVersion represents a version of a notifier plugin, including configuration options and metadata.
type NotifierPluginVersion struct {
Version string `json:"version"`
CanCreate bool `json:"canCreate"`
Options []NotifierOption `json:"options"`
Info string `json:"info"`
TypeAlias string `json:"typeAlias,omitempty"`
Version NotifierVersion `json:"version"`
CanCreate bool `json:"canCreate"`
Options []NotifierOption `json:"options"`
Info string `json:"info,omitempty"`
Plugin *VersionedNotifierPlugin `json:"-"`
}
// GetSecretFieldsPaths returns a list of paths for fields marked as secure within the NotifierPluginVersion's options.
func (v NotifierPluginVersion) GetSecretFieldsPaths() []string {
return getSecretFields("", v.Options)
}
// NotifierOption holds information about options specific for the NotifierPlugin.

View File

@ -107,7 +107,7 @@ func encryptReceiverConfigs(c []*definitions.PostableApiReceiver, encrypt defini
return fmt.Errorf("integration '%s' of receiver '%s' has settings that cannot be parsed as JSON: %w", gr.Type, gr.Name, err)
}
secretKeys, err := channels_config.GetSecretKeysForContactPointType(gr.Type)
secretKeys, err := channels_config.GetSecretKeysForContactPointType(gr.Type, channels_config.V1)
if err != nil {
return fmt.Errorf("failed to get secret keys for contact point type %s: %w", gr.Type, err)
}

View File

@ -247,7 +247,7 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in
if err != nil {
return err
}
secretKeys, err := channels_config.GetSecretKeysForContactPointType(contactPoint.Type)
secretKeys, err := channels_config.GetSecretKeysForContactPointType(contactPoint.Type, channels_config.V1)
if err != nil {
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
}
@ -522,7 +522,7 @@ func ValidateContactPoint(ctx context.Context, e apimodels.EmbeddedContactPoint,
// RemoveSecretsForContactPoint removes all secrets from the contact point's settings and returns them as a map. Returns error if contact point type is not known.
func RemoveSecretsForContactPoint(e *apimodels.EmbeddedContactPoint) (map[string]string, error) {
s := map[string]string{}
secretKeys, err := channels_config.GetSecretKeysForContactPointType(e.Type)
secretKeys, err := channels_config.GetSecretKeysForContactPointType(e.Type, channels_config.V1)
if err != nil {
return nil, err
}

View File

@ -448,7 +448,7 @@ func TestRemoveSecretsForContactPoint(t *testing.T) {
settingsRaw, err := json.Marshal(integration.Settings)
require.NoError(t, err)
expectedFields, err := channels_config.GetSecretKeysForContactPointType(integrationType)
expectedFields, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1)
require.NoError(t, err)
t.Run(integrationType, func(t *testing.T) {

View File

@ -1,12 +1,16 @@
package alerting
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
@ -21,14 +25,14 @@ func TestIntegrationAvailableChannels(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
// Create a user to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
@ -37,20 +41,48 @@ func TestIntegrationAvailableChannels(t *testing.T) {
Login: "grafana",
})
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
t.Run("should return all available notifiers", func(t *testing.T) {
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
expNotifiers := channels_config.GetAvailableNotifiers()
expJson, err := json.Marshal(expNotifiers)
require.NoError(t, err)
require.Equal(t, string(expJson), string(b))
expNotifiers := channels_config.GetAvailableNotifiers()
expJson, err := json.Marshal(expNotifiers)
require.NoError(t, err)
require.Equal(t, string(expJson), string(b))
})
t.Run("should return versioned notifiers", func(t *testing.T) {
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers?version=2", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
expectedBytes, err := os.ReadFile(path.Join("test-data", "alert-notifiers-v2-snapshot.json"))
require.NoError(t, err)
require.NoError(t, err)
if !assert.JSONEq(t, string(expectedBytes), string(b)) {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, b, "", " ")
require.NoError(t, err)
err = os.WriteFile(path.Join("test-data", "alert-notifiers-v2-snapshot.json"), prettyJSON.Bytes(), 0o644)
require.NoError(t, err)
}
})
}

File diff suppressed because it is too large Load Diff

View File

@ -1318,7 +1318,7 @@ func TestIntegrationCRUD(t *testing.T) {
expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)]
var fields map[string]any
require.NoError(t, json.Unmarshal([]byte(expected.Config), &fields))
secretFields, err := channels_config.GetSecretKeysForContactPointType(integration.Type)
secretFields, err := channels_config.GetSecretKeysForContactPointType(integration.Type, channels_config.V1)
require.NoError(t, err)
for _, field := range secretFields {
if _, ok := fields[field]; !ok { // skip field that is not in the original setting