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 ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"slices" | 	"slices" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/api/response" | 	"github.com/grafana/grafana/pkg/api/response" | ||||||
| 	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" | 	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" | ||||||
|  | @ -12,7 +13,10 @@ import ( | ||||||
| func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) response.Response { | func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) response.Response { | ||||||
| 	return func(r *contextmodel.ReqContext) response.Response { | 	return func(r *contextmodel.ReqContext) response.Response { | ||||||
| 		if r.Query("version") == "2" { | 		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()) | 		return response.JSON(http.StatusOK, channels_config.GetAvailableNotifiers()) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -228,23 +228,21 @@ func (f IntegrationFieldPath) With(segment string) IntegrationFieldPath { | ||||||
| //	IntegrationConfig - The integration configuration
 | //	IntegrationConfig - The integration configuration
 | ||||||
| //	error - Error if integration type not found or invalid version specified
 | //	error - Error if integration type not found or invalid version specified
 | ||||||
| func IntegrationConfigFromType(integrationType string, version *string) (IntegrationConfig, error) { | func IntegrationConfigFromType(integrationType string, version *string) (IntegrationConfig, error) { | ||||||
| 	config, err := channels_config.ConfigForIntegrationType(integrationType) | 	versionConfig, err := channels_config.ConfigForIntegrationType(integrationType) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return IntegrationConfig{}, err | 		return IntegrationConfig{}, err | ||||||
| 	} | 	} | ||||||
| 	var versionConfig channels_config.NotifierPluginVersion | 	// if particular version is requested and the version returned does not match, try to get the correct version
 | ||||||
| 	if version == nil { | 	if version != nil && *version != string(versionConfig.Version) { | ||||||
| 		versionConfig = config.GetCurrentVersion() | 		exists := false | ||||||
| 	} else { | 		versionConfig, exists = versionConfig.Plugin.GetVersion(channels_config.NotifierVersion(*version)) | ||||||
| 		var ok bool | 		if !exists { | ||||||
| 		versionConfig, ok = config.GetVersion(*version) |  | ||||||
| 		if !ok { |  | ||||||
| 			return IntegrationConfig{}, fmt.Errorf("version %s not found in config", *version) | 			return IntegrationConfig{}, fmt.Errorf("version %s not found in config", *version) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	integrationConfig := IntegrationConfig{ | 	integrationConfig := IntegrationConfig{ | ||||||
| 		Type:    config.Type, | 		Type:    versionConfig.Plugin.Type, | ||||||
| 		Version: versionConfig.Version, | 		Version: string(versionConfig.Version), | ||||||
| 		Fields:  make(map[string]IntegrationField, len(versionConfig.Options)), | 		Fields:  make(map[string]IntegrationField, len(versionConfig.Options)), | ||||||
| 	} | 	} | ||||||
| 	for _, option := range versionConfig.Options { | 	for _, option := range versionConfig.Options { | ||||||
|  |  | ||||||
|  | @ -46,7 +46,7 @@ func TestReceiver_EncryptDecrypt(t *testing.T) { | ||||||
| 			decrypedIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() | 			decrypedIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() | ||||||
| 
 | 
 | ||||||
| 			encrypted := decrypedIntegration.Clone() | 			encrypted := decrypedIntegration.Clone() | ||||||
| 			secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType) | 			secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 			for _, key := range secrets { | 			for _, key := range secrets { | ||||||
| 				val, ok, err := extractField(encrypted.Settings, NewIntegrationFieldPath(key)) | 				val, ok, err := extractField(encrypted.Settings, NewIntegrationFieldPath(key)) | ||||||
|  | @ -82,7 +82,7 @@ func TestIntegration_Redact(t *testing.T) { | ||||||
| 			validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() | 			validIntegration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() | ||||||
| 
 | 
 | ||||||
| 			expected := validIntegration.Clone() | 			expected := validIntegration.Clone() | ||||||
| 			secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType) | 			secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 			for _, key := range secrets { | 			for _, key := range secrets { | ||||||
| 				err := setField(expected.Settings, NewIntegrationFieldPath(key), func(current any) any { | 				err := setField(expected.Settings, NewIntegrationFieldPath(key), func(current any) any { | ||||||
|  | @ -247,12 +247,12 @@ func TestSecretsIntegrationConfig(t *testing.T) { | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 			t.Run("v1 is current", func(t *testing.T) { | 			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.NoError(t, err) | ||||||
| 				assert.Equal(t, config, configv1) | 				assert.Equal(t, config, configv1) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType) | 			secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 			allSecrets := make(map[string]struct{}, len(secrets)) | 			allSecrets := make(map[string]struct{}, len(secrets)) | ||||||
| 			for _, key := range secrets { | 			for _, key := range secrets { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"iter" | 	"iter" | ||||||
| 	"maps" | 	"maps" | ||||||
| 	"os" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/alerting/receivers/jira" | 	"github.com/grafana/alerting/receivers/jira" | ||||||
|  | @ -14,10 +13,20 @@ import ( | ||||||
| 	alertingTemplates "github.com/grafana/alerting/templates" | 	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.
 | // GetAvailableNotifiers returns the metadata of all the notification channels that can be configured.
 | ||||||
| func GetAvailableNotifiers() []*NotifierPlugin { | func GetAvailableNotifiers() []*NotifierPlugin { | ||||||
| 	hostname, _ := os.Hostname() |  | ||||||
| 
 |  | ||||||
| 	pushoverSoundOptions := []SelectOption{ | 	pushoverSoundOptions := []SelectOption{ | ||||||
| 		{ | 		{ | ||||||
| 			Value: "default", | 			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", | 					Description:  "The unique location of the affected system, preferably a hostname or FQDN. You can use templates", | ||||||
| 					Element:      ElementTypeInput, | 					Element:      ElementTypeInput, | ||||||
| 					InputType:    InputTypeText, | 					InputType:    InputTypeText, | ||||||
| 					Placeholder:  hostname, | 					Placeholder:  "grafana.local", | ||||||
| 					PropertyName: "source", | 					PropertyName: "source", | ||||||
| 				}, | 				}, | ||||||
| 				{ // New in 9.4.
 | 				{ // 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.
 | // 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) { | func GetSecretKeysForContactPointType(contactPointType string, version NotifierVersion) ([]string, error) { | ||||||
| 	notifiers := GetAvailableNotifiers() | 	var notifiers []*NotifierPlugin | ||||||
|  | 	if version == V1 { | ||||||
|  | 		notifiers = GetAvailableNotifiers() | ||||||
|  | 	} | ||||||
|  | 	if version == V0mimir1 { | ||||||
|  | 		notifiers = getAvailableV0mimir1Notifiers() | ||||||
|  | 	} | ||||||
|  | 	if version == V0mimir2 { | ||||||
|  | 		notifiers = getAvailableV0mimir2Notifiers() | ||||||
|  | 	} | ||||||
| 	for _, n := range notifiers { | 	for _, n := range notifiers { | ||||||
| 		if strings.EqualFold(n.Type, contactPointType) { | 		if strings.EqualFold(n.Type, contactPointType) { | ||||||
| 			return getSecretFields("", n.Options), nil | 			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 { | 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.
 | // 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() | 	notifiers := GetAvailableNotifiersV2() | ||||||
| 	for n := range notifiers { | 	for n := range notifiers { | ||||||
| 		if strings.EqualFold(n.Type, contactPointType) { | 		if strings.EqualFold(n.Type, contactPointTypeOrAlternateType) { | ||||||
| 			return n, nil | 			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] { | func GetAvailableNotifiersV2() iter.Seq[*VersionedNotifierPlugin] { | ||||||
| 	v1 := GetAvailableNotifiers() | 	m := make(map[string]*VersionedNotifierPlugin, 24) // we support 24 notifier types
 | ||||||
| 	m := make(map[string]VersionedNotifierPlugin, len(v1)) | 	add := func(n []*NotifierPlugin, version NotifierVersion) { | ||||||
| 	for _, n := range v1 { | 		for _, n := range n { | ||||||
| 		pl := VersionedNotifierPlugin{ | 			pl, ok := m[n.Type] | ||||||
| 			Type:           n.Type, | 			if !ok { | ||||||
| 			Name:           n.Name, | 				pl = &VersionedNotifierPlugin{ | ||||||
| 			Description:    n.Description, | 					Type:           n.Type, | ||||||
| 			Heading:        n.Heading, | 					Name:           n.Name, | ||||||
| 			Info:           n.Info, | 					Description:    n.Description, | ||||||
| 			CurrentVersion: "v1", | 					Heading:        n.Heading, | ||||||
| 			Versions: []NotifierPluginVersion{ | 					Info:           n.Info, | ||||||
| 				{ | 					CurrentVersion: version, | ||||||
| 					Version:   "v1", | 					Versions:       make([]NotifierPluginVersion, 0, 2), // usually, there are 2 versions per type
 | ||||||
| 					CanCreate: true, | 				} | ||||||
| 					Options:   n.Options, | 				m[n.Type] = pl | ||||||
| 					Info:      "", | 			} | ||||||
| 				}, | 			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) | 	return maps.Values(m) | ||||||
| } | } | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,28 +1,38 @@ | ||||||
| package channels_config | package channels_config | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"maps" | ||||||
|  | 	"reflect" | ||||||
|  | 	"slices" | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/grafana/alerting/notify/notifytest" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestGetSecretKeysForContactPointType(t *testing.T) { | func TestGetSecretKeysForContactPointType(t *testing.T) { | ||||||
|  | 	httpConfigSecrets := []string{"http_config.authorization.credentials", "http_config.basic_auth.password", "http_config.oauth2.client_secret"} | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		receiverType         string | 		receiverType         string | ||||||
|  | 		version              NotifierVersion | ||||||
| 		expectedSecretFields []string | 		expectedSecretFields []string | ||||||
| 	}{ | 	}{ | ||||||
| 		{receiverType: "dingding", expectedSecretFields: []string{"url"}}, | 		{receiverType: "dingding", version: V1, expectedSecretFields: []string{"url"}}, | ||||||
| 		{receiverType: "kafka", expectedSecretFields: []string{"password"}}, | 		{receiverType: "kafka", version: V1, expectedSecretFields: []string{"password"}}, | ||||||
| 		{receiverType: "email", expectedSecretFields: []string{}}, | 		{receiverType: "email", version: V1, expectedSecretFields: []string{}}, | ||||||
| 		{receiverType: "pagerduty", expectedSecretFields: []string{"integrationKey"}}, | 		{receiverType: "pagerduty", version: V1, expectedSecretFields: []string{"integrationKey"}}, | ||||||
| 		{receiverType: "victorops", expectedSecretFields: []string{"url"}}, | 		{receiverType: "victorops", version: V1, expectedSecretFields: []string{"url"}}, | ||||||
| 		{receiverType: "oncall", expectedSecretFields: []string{"password", "authorization_credentials"}}, | 		{receiverType: "oncall", version: V1, expectedSecretFields: []string{"password", "authorization_credentials"}}, | ||||||
| 		{receiverType: "pushover", expectedSecretFields: []string{"apiToken", "userKey"}}, | 		{receiverType: "pushover", version: V1, expectedSecretFields: []string{"apiToken", "userKey"}}, | ||||||
| 		{receiverType: "slack", expectedSecretFields: []string{"token", "url"}}, | 		{receiverType: "slack", version: V1, expectedSecretFields: []string{"token", "url"}}, | ||||||
| 		{receiverType: "sensugo", expectedSecretFields: []string{"apikey"}}, | 		{receiverType: "sensugo", version: V1, expectedSecretFields: []string{"apikey"}}, | ||||||
| 		{receiverType: "teams", expectedSecretFields: []string{}}, | 		{receiverType: "teams", version: V1, expectedSecretFields: []string{}}, | ||||||
| 		{receiverType: "telegram", expectedSecretFields: []string{"bottoken"}}, | 		{receiverType: "telegram", version: V1, expectedSecretFields: []string{"bottoken"}}, | ||||||
| 		{receiverType: "webhook", expectedSecretFields: []string{ | 		{receiverType: "webhook", version: V1, expectedSecretFields: []string{ | ||||||
| 			"password", | 			"password", | ||||||
| 			"authorization_credentials", | 			"authorization_credentials", | ||||||
| 			"tlsConfig.caCertificate", | 			"tlsConfig.caCertificate", | ||||||
|  | @ -34,44 +44,147 @@ func TestGetSecretKeysForContactPointType(t *testing.T) { | ||||||
| 			"http_config.oauth2.tls_config.clientCertificate", | 			"http_config.oauth2.tls_config.clientCertificate", | ||||||
| 			"http_config.oauth2.tls_config.clientKey", | 			"http_config.oauth2.tls_config.clientKey", | ||||||
| 		}}, | 		}}, | ||||||
| 		{receiverType: "wecom", expectedSecretFields: []string{"url", "secret"}}, | 		{receiverType: "wecom", version: V1, expectedSecretFields: []string{"url", "secret"}}, | ||||||
| 		{receiverType: "prometheus-alertmanager", expectedSecretFields: []string{"basicAuthPassword"}}, | 		{receiverType: "prometheus-alertmanager", version: V1, expectedSecretFields: []string{"basicAuthPassword"}}, | ||||||
| 		{receiverType: "discord", expectedSecretFields: []string{"url"}}, | 		{receiverType: "discord", version: V1, expectedSecretFields: []string{"url"}}, | ||||||
| 		{receiverType: "googlechat", expectedSecretFields: []string{"url"}}, | 		{receiverType: "googlechat", version: V1, expectedSecretFields: []string{"url"}}, | ||||||
| 		{receiverType: "LINE", expectedSecretFields: []string{"token"}}, | 		{receiverType: "LINE", version: V1, expectedSecretFields: []string{"token"}}, | ||||||
| 		{receiverType: "threema", expectedSecretFields: []string{"api_secret"}}, | 		{receiverType: "threema", version: V1, expectedSecretFields: []string{"api_secret"}}, | ||||||
| 		{receiverType: "opsgenie", expectedSecretFields: []string{"apiKey"}}, | 		{receiverType: "opsgenie", version: V1, expectedSecretFields: []string{"apiKey"}}, | ||||||
| 		{receiverType: "webex", expectedSecretFields: []string{"bot_token"}}, | 		{receiverType: "webex", version: V1, expectedSecretFields: []string{"bot_token"}}, | ||||||
| 		{receiverType: "sns", expectedSecretFields: []string{"sigv4.access_key", "sigv4.secret_key"}}, | 		{receiverType: "sns", version: V1, expectedSecretFields: []string{"sigv4.access_key", "sigv4.secret_key"}}, | ||||||
| 		{receiverType: "mqtt", expectedSecretFields: []string{"password", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey"}}, | 		{receiverType: "mqtt", version: V1, expectedSecretFields: []string{"password", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey"}}, | ||||||
| 		{receiverType: "jira", expectedSecretFields: []string{"user", "password", "api_token"}}, | 		{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() | 	n := slices.Collect(GetAvailableNotifiersV2()) | ||||||
| 	allTypes := make(map[string]struct{}, len(n)) | 	type typeWithVersion struct { | ||||||
| 	for _, plugin := range n { | 		Type    string | ||||||
| 		allTypes[plugin.Type] = struct{}{} | 		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 { | 	for _, testCase := range testCases { | ||||||
| 		delete(allTypes, testCase.receiverType) | 		delete(allTypes, getKey(testCase.receiverType, testCase.version)) | ||||||
| 		t.Run(testCase.receiverType, func(t *testing.T) { | 		t.Run(fmt.Sprintf("%s-%s", testCase.receiverType, testCase.version), func(t *testing.T) { | ||||||
| 			got, err := GetSecretKeysForContactPointType(testCase.receiverType) | 			got, err := GetSecretKeysForContactPointType(testCase.receiverType, testCase.version) | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
| 			require.ElementsMatch(t, testCase.expectedSecretFields, got) | 			require.ElementsMatch(t, testCase.expectedSecretFields, got) | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for integrationType := range allTypes { | 	for it := range allTypes { | ||||||
| 		t.Run(integrationType, func(t *testing.T) { | 		t.Run(fmt.Sprintf("%s-%s", it.Type, it.Version), func(t *testing.T) { | ||||||
| 			got, err := GetSecretKeysForContactPointType(integrationType) | 			got, err := GetSecretKeysForContactPointType(it.Type, it.Version) | ||||||
| 			require.NoError(t, err) | 			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) | 	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) { | func Test_getSecretFields(t *testing.T) { | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		name           string | 		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.
 | // NotifierPlugin holds meta information about a notifier.
 | ||||||
| type NotifierPlugin struct { | type NotifierPlugin struct { | ||||||
| 	Type        string           `json:"type"` | 	Type        string           `json:"type"` | ||||||
|  | 	TypeAlias   string           `json:"typeAlias,omitempty"` | ||||||
| 	Name        string           `json:"name"` | 	Name        string           `json:"name"` | ||||||
| 	Heading     string           `json:"heading"` | 	Heading     string           `json:"heading"` | ||||||
| 	Description string           `json:"description"` | 	Description string           `json:"description"` | ||||||
|  | @ -14,16 +15,16 @@ type NotifierPlugin struct { | ||||||
| // It includes metadata such as type, name, description, and version-specific details.
 | // It includes metadata such as type, name, description, and version-specific details.
 | ||||||
| type VersionedNotifierPlugin struct { | type VersionedNotifierPlugin struct { | ||||||
| 	Type           string                  `json:"type"` | 	Type           string                  `json:"type"` | ||||||
| 	CurrentVersion string                  `json:"currentVersion"` | 	CurrentVersion NotifierVersion         `json:"currentVersion"` | ||||||
| 	Name           string                  `json:"name"` | 	Name           string                  `json:"name"` | ||||||
| 	Heading        string                  `json:"heading"` | 	Heading        string                  `json:"heading,omitempty"` | ||||||
| 	Description    string                  `json:"description"` | 	Description    string                  `json:"description,omitempty"` | ||||||
| 	Info           string                  `json:"info"` | 	Info           string                  `json:"info,omitempty"` | ||||||
| 	Versions       []NotifierPluginVersion `json:"versions"` | 	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.
 | // 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 { | 	for _, version := range p.Versions { | ||||||
| 		if version.Version == v { | 		if version.Version == v { | ||||||
| 			return version, true | 			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.
 | // NotifierPluginVersion represents a version of a notifier plugin, including configuration options and metadata.
 | ||||||
| type NotifierPluginVersion struct { | type NotifierPluginVersion struct { | ||||||
| 	Version   string           `json:"version"` | 	TypeAlias string                   `json:"typeAlias,omitempty"` | ||||||
| 	CanCreate bool             `json:"canCreate"` | 	Version   NotifierVersion          `json:"version"` | ||||||
| 	Options   []NotifierOption `json:"options"` | 	CanCreate bool                     `json:"canCreate"` | ||||||
| 	Info      string           `json:"info"` | 	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.
 | // 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) | 						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 { | 					if err != nil { | ||||||
| 						return fmt.Errorf("failed to get secret keys for contact point type %s: %w", gr.Type, err) | 						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 { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	secretKeys, err := channels_config.GetSecretKeysForContactPointType(contactPoint.Type) | 	secretKeys, err := channels_config.GetSecretKeysForContactPointType(contactPoint.Type, channels_config.V1) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%w: %s", ErrValidation, err.Error()) | 		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.
 | // 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) { | func RemoveSecretsForContactPoint(e *apimodels.EmbeddedContactPoint) (map[string]string, error) { | ||||||
| 	s := map[string]string{} | 	s := map[string]string{} | ||||||
| 	secretKeys, err := channels_config.GetSecretKeysForContactPointType(e.Type) | 	secretKeys, err := channels_config.GetSecretKeysForContactPointType(e.Type, channels_config.V1) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -448,7 +448,7 @@ func TestRemoveSecretsForContactPoint(t *testing.T) { | ||||||
| 		settingsRaw, err := json.Marshal(integration.Settings) | 		settingsRaw, err := json.Marshal(integration.Settings) | ||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 		expectedFields, err := channels_config.GetSecretKeysForContactPointType(integrationType) | 		expectedFields, err := channels_config.GetSecretKeysForContactPointType(integrationType, channels_config.V1) | ||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 		t.Run(integrationType, func(t *testing.T) { | 		t.Run(integrationType, func(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -1,12 +1,16 @@ | ||||||
| package alerting | package alerting | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" | 	"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" | ||||||
|  | @ -21,14 +25,14 @@ func TestIntegrationAvailableChannels(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	testinfra.SQLiteIntegrationTest(t) | 	testinfra.SQLiteIntegrationTest(t) | ||||||
| 
 | 
 | ||||||
| 	dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ | 	dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ | ||||||
| 		DisableLegacyAlerting: true, | 		DisableLegacyAlerting: true, | ||||||
| 		EnableUnifiedAlerting: true, | 		EnableUnifiedAlerting: true, | ||||||
| 		DisableAnonymous:      true, | 		DisableAnonymous:      true, | ||||||
| 		AppModeProduction:     true, | 		AppModeProduction:     true, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) | 	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) | ||||||
| 
 | 
 | ||||||
| 	// Create a user to make authenticated requests
 | 	// Create a user to make authenticated requests
 | ||||||
| 	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ | 	createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ | ||||||
|  | @ -37,20 +41,48 @@ func TestIntegrationAvailableChannels(t *testing.T) { | ||||||
| 		Login:          "grafana", | 		Login:          "grafana", | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr) | 	t.Run("should return all available notifiers", func(t *testing.T) { | ||||||
| 	// nolint:gosec
 | 		alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr) | ||||||
| 	resp, err := http.Get(alertsURL) | 		// nolint:gosec
 | ||||||
| 	require.NoError(t, err) | 		resp, err := http.Get(alertsURL) | ||||||
| 	t.Cleanup(func() { |  | ||||||
| 		err := resp.Body.Close() |  | ||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| 	}) | 		t.Cleanup(func() { | ||||||
| 	b, err := io.ReadAll(resp.Body) | 			err := resp.Body.Close() | ||||||
| 	require.NoError(t, err) | 			require.NoError(t, err) | ||||||
| 	require.Equal(t, 200, resp.StatusCode) | 		}) | ||||||
|  | 		b, err := io.ReadAll(resp.Body) | ||||||
|  | 		require.NoError(t, err) | ||||||
|  | 		require.Equal(t, 200, resp.StatusCode) | ||||||
| 
 | 
 | ||||||
| 	expNotifiers := channels_config.GetAvailableNotifiers() | 		expNotifiers := channels_config.GetAvailableNotifiers() | ||||||
| 	expJson, err := json.Marshal(expNotifiers) | 		expJson, err := json.Marshal(expNotifiers) | ||||||
| 	require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| 	require.Equal(t, string(expJson), string(b)) | 		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)] | 					expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)] | ||||||
| 					var fields map[string]any | 					var fields map[string]any | ||||||
| 					require.NoError(t, json.Unmarshal([]byte(expected.Config), &fields)) | 					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) | 					require.NoError(t, err) | ||||||
| 					for _, field := range secretFields { | 					for _, field := range secretFields { | ||||||
| 						if _, ok := fields[field]; !ok { // skip field that is not in the original setting
 | 						if _, ok := fields[field]; !ok { // skip field that is not in the original setting
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue