mirror of https://github.com/grafana/grafana.git
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:
parent
a6db37c2b7
commit
c36b2ae191
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue