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{ | ||||
| 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: "v1", | ||||
| 			Versions: []NotifierPluginVersion{ | ||||
| 				{ | ||||
| 					Version:   "v1", | ||||
| 					CanCreate: true, | ||||
| 					Options:   n.Options, | ||||
| 					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, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 	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 := 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{}{} | ||||
| 		} | ||||
| 	n := GetAvailableNotifiers() | ||||
| 	allTypes := make(map[string]struct{}, len(n)) | ||||
| 	for _, plugin := range n { | ||||
| 		allTypes[plugin.Type] = 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"` | ||||
| 	TypeAlias string                   `json:"typeAlias,omitempty"` | ||||
| 	Version   NotifierVersion          `json:"version"` | ||||
| 	CanCreate bool                     `json:"canCreate"` | ||||
| 	Options   []NotifierOption         `json:"options"` | ||||
| 	Info      string           `json:"info"` | ||||
| 	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,6 +41,7 @@ func TestIntegrationAvailableChannels(t *testing.T) { | |||
| 		Login:          "grafana", | ||||
| 	}) | ||||
| 
 | ||||
| 	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) | ||||
|  | @ -53,4 +58,31 @@ func TestIntegrationAvailableChannels(t *testing.T) { | |||
| 		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