diff --git a/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue b/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue index 66f78662617..8be089f8f61 100644 --- a/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue +++ b/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue @@ -8,6 +8,7 @@ ReceiverSpec: { #Integration: { uid?: string type: string + version: string disableResolveMessage?: bool settings: { [string]: _ diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go index d43a3c51883..aaef41abfd5 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go @@ -6,6 +6,7 @@ package v0alpha1 type ReceiverIntegration struct { Uid *string `json:"uid,omitempty"` Type string `json:"type"` + Version string `json:"version"` DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"` Settings map[string]interface{} `json:"settings"` SecureFields map[string]bool `json:"secureFields,omitempty"` diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go index e79028361b2..5190f68f81d 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go @@ -108,6 +108,13 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceC Format: "", }, }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "disableResolveMessage": { SchemaProps: spec.SchemaProps{ Type: []string{"boolean"}, @@ -144,7 +151,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceC }, }, }, - Required: []string{"type", "settings"}, + Required: []string{"type", "version", "settings"}, }, }, } diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 1c7b9c6dcd3..abe0c5bf177 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "slices" "github.com/grafana/grafana/pkg/api/response" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -9,7 +10,10 @@ import ( ) func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) response.Response { - return 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())) + } return response.JSON(http.StatusOK, channels_config.GetAvailableNotifiers()) } } diff --git a/pkg/registry/apps/alerting/notifications/receiver/conversions.go b/pkg/registry/apps/alerting/notifications/receiver/conversions.go index 6aaa1b18e58..04b85a5af79 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/conversions.go +++ b/pkg/registry/apps/alerting/notifications/receiver/conversions.go @@ -68,6 +68,7 @@ func convertToK8sResource( DisableResolveMessage: &integration.DisableResolveMessage, Settings: maps.Clone(integration.Settings), SecureFields: integration.SecureFields(), + Version: integration.Config.Version, }) } @@ -120,10 +121,13 @@ func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[str Version: receiver.ResourceVersion, Provenance: ngmodels.ProvenanceNone, } - storedSecureFields := make(map[string][]string, len(receiver.Spec.Integrations)) for _, integration := range receiver.Spec.Integrations { - config, err := ngmodels.IntegrationConfigFromType(integration.Type) + version := &integration.Version + if *version == "" { + version = nil + } + config, err := ngmodels.IntegrationConfigFromType(integration.Type, version) if err != nil { return nil, nil, err } diff --git a/pkg/services/ngalert/models/receivers.go b/pkg/services/ngalert/models/receivers.go index 044a701c482..3a0996c68f6 100644 --- a/pkg/services/ngalert/models/receivers.go +++ b/pkg/services/ngalert/models/receivers.go @@ -147,8 +147,9 @@ type Integration struct { // IntegrationConfig represents the configuration of an integration. It contains the type and information about the fields. type IntegrationConfig struct { - Type string - Fields map[string]IntegrationField + Type string + Version string + Fields map[string]IntegrationField } // IntegrationField represents a field in an integration configuration. @@ -191,17 +192,40 @@ func (f IntegrationFieldPath) With(segment string) IntegrationFieldPath { return newPath } -// IntegrationConfigFromType returns an integration configuration for a given integration type. If the integration type is -// not found an error is returned. -func IntegrationConfigFromType(integrationType string) (IntegrationConfig, error) { +// IntegrationConfigFromType returns an integration configuration for a given integration type of a given version. +// If version is nil, the current version of the integration is used. +// Returns an error if the integration type is not found or if the specified version does not exist. +// +// Parameters: +// +// integrationType - The type of integration to get configuration for +// version - Optional specific version to get configuration for, uses latest if nil +// +// Returns: +// +// 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) if err != nil { return IntegrationConfig{}, err } - - integrationConfig := IntegrationConfig{Type: config.Type, Fields: make(map[string]IntegrationField, len(config.Options))} - - for _, option := range config.Options { + var versionConfig channels_config.NotifierPluginVersion + if version == nil { + versionConfig = config.GetCurrentVersion() + } else { + var ok bool + versionConfig, ok = config.GetVersion(*version) + if !ok { + return IntegrationConfig{}, fmt.Errorf("version %s not found in config", *version) + } + } + integrationConfig := IntegrationConfig{ + Type: config.Type, + Version: versionConfig.Version, + Fields: make(map[string]IntegrationField, len(versionConfig.Options)), + } + for _, option := range versionConfig.Options { integrationConfig.Fields[option.PropertyName] = notifierOptionToIntegrationField(option) } return integrationConfig, nil @@ -259,7 +283,8 @@ func traverseFields(flds map[string]IntegrationField, parentPath IntegrationFiel func (config *IntegrationConfig) Clone() IntegrationConfig { clone := IntegrationConfig{ - Type: config.Type, + Type: config.Type, + Version: config.Version, } if len(config.Fields) > 0 { diff --git a/pkg/services/ngalert/models/receivers_test.go b/pkg/services/ngalert/models/receivers_test.go index 4d1b09bd4ef..a256ecfb2f9 100644 --- a/pkg/services/ngalert/models/receivers_test.go +++ b/pkg/services/ngalert/models/receivers_test.go @@ -1,6 +1,7 @@ package models import ( + "maps" "reflect" "testing" @@ -9,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" + "github.com/grafana/grafana/pkg/util" ) func TestReceiver_Clone(t *testing.T) { @@ -241,17 +243,19 @@ func TestIntegration_WithExistingSecureFields(t *testing.T) { } } -func TestIntegrationConfig(t *testing.T) { +func TestSecretsIntegrationConfig(t *testing.T) { // Test that all known integration types have a config and correctly mark their secrets as secure. - if testing.Short() { - t.Skip("skipping integration test in short mode") - } - for integrationType := range alertingNotify.AllKnownConfigsForTesting { t.Run(integrationType, func(t *testing.T) { - config, err := IntegrationConfigFromType(integrationType) + config, err := IntegrationConfigFromType(integrationType, nil) assert.NoError(t, err) + t.Run("v1 is current", func(t *testing.T) { + configv1, err := IntegrationConfigFromType(integrationType, util.Pointer("v1")) + assert.NoError(t, err) + assert.Equal(t, config, configv1) + }) + secrets, err := channels_config.GetSecretKeysForContactPointType(integrationType) assert.NoError(t, err) allSecrets := make(map[string]struct{}, len(secrets)) @@ -271,9 +275,18 @@ func TestIntegrationConfig(t *testing.T) { } t.Run("Unknown type returns error", func(t *testing.T) { - _, err := IntegrationConfigFromType("__--**unknown_type**--__") + _, err := IntegrationConfigFromType("__--**unknown_type**--__", nil) assert.Error(t, err) }) + + t.Run("Unknown version returns error", func(t *testing.T) { + version := util.Pointer("__--**unknown_version**--__") + types := maps.Keys(alertingNotify.AllKnownConfigsForTesting) + for itype := range types { + _, err := IntegrationConfigFromType(itype, version) + assert.Errorf(t, err, "unknown version for integration type %s did not return error but should", itype) + } + }) } func TestIntegration_SecureFields(t *testing.T) { diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index e459b1cc1a7..19a26ec8c53 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -1210,6 +1210,7 @@ func (n ReceiverMutators) WithProvenance(provenance Provenance) Mutator[Receiver func (n ReceiverMutators) WithValidIntegration(integrationType string) Mutator[Receiver] { return func(r *Receiver) { + // TODO add support for v0 integration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))() r.Integrations = []*Integration{&integration} } @@ -1217,6 +1218,7 @@ func (n ReceiverMutators) WithValidIntegration(integrationType string) Mutator[R func (n ReceiverMutators) WithInvalidIntegration(integrationType string) Mutator[Receiver] { return func(r *Receiver) { + // TODO add support for v0 integration := IntegrationGen(IntegrationMuts.WithInvalidConfig(integrationType))() r.Integrations = []*Integration{&integration} } @@ -1305,8 +1307,9 @@ func (n IntegrationMutators) WithName(name string) Mutator[Integration] { func (n IntegrationMutators) WithValidConfig(integrationType string) Mutator[Integration] { return func(c *Integration) { + // TODO add support for v0 integrations config := alertingNotify.AllKnownConfigsForTesting[integrationType].GetRawNotifierConfig(c.Name) - integrationConfig, _ := IntegrationConfigFromType(integrationType) + integrationConfig, _ := IntegrationConfigFromType(integrationType, nil) c.Config = integrationConfig var settings map[string]any @@ -1324,7 +1327,7 @@ func (n IntegrationMutators) WithValidConfig(integrationType string) Mutator[Int func (n IntegrationMutators) WithInvalidConfig(integrationType string) Mutator[Integration] { return func(c *Integration) { - integrationConfig, _ := IntegrationConfigFromType(integrationType) + integrationConfig, _ := IntegrationConfigFromType(integrationType, nil) c.Config = integrationConfig c.Settings = map[string]interface{}{} c.SecureSettings = map[string]string{} diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index d69fa8fc7f6..6e7182e1f10 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -2,6 +2,8 @@ package channels_config import ( "fmt" + "iter" + "maps" "os" "strings" @@ -2082,12 +2084,37 @@ 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) (*NotifierPlugin, error) { - notifiers := GetAvailableNotifiers() - for _, n := range notifiers { +func ConfigForIntegrationType(contactPointType string) (VersionedNotifierPlugin, error) { + notifiers := GetAvailableNotifiersV2() + for n := range notifiers { if strings.EqualFold(n.Type, contactPointType) { return n, nil } } - return nil, fmt.Errorf("unknown integration type '%s'", contactPointType) + return VersionedNotifierPlugin{}, fmt.Errorf("unknown integration type '%s'", contactPointType) +} + +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: "", + }, + }, + } + m[n.Type] = pl + } + return maps.Values(m) } diff --git a/pkg/services/ngalert/notifier/channels_config/plugin.go b/pkg/services/ngalert/notifier/channels_config/plugin.go index 5a8b7527604..f7332bdcfd2 100644 --- a/pkg/services/ngalert/notifier/channels_config/plugin.go +++ b/pkg/services/ngalert/notifier/channels_config/plugin.go @@ -10,6 +10,46 @@ type NotifierPlugin struct { Options []NotifierOption `json:"options"` } +// VersionedNotifierPlugin represents a notifier plugin with multiple versions and detailed configuration options. +// It includes metadata such as type, name, description, and version-specific details. +type VersionedNotifierPlugin struct { + Type string `json:"type"` + CurrentVersion string `json:"currentVersion"` + Name string `json:"name"` + Heading string `json:"heading"` + Description string `json:"description"` + Info string `json:"info"` + 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) { + for _, version := range p.Versions { + if version.Version == v { + return version, true + } + } + return NotifierPluginVersion{}, false +} + +// GetCurrentVersion retrieves the current version of the notifier plugin based on the CurrentVersion property. +// Panics if the version specified in CurrentVersion is not found in the configured versions. +func (p VersionedNotifierPlugin) GetCurrentVersion() NotifierPluginVersion { + v, ok := p.GetVersion(p.CurrentVersion) + if !ok { + panic("version not found for current version: " + p.CurrentVersion) + } + return v +} + +// 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"` +} + // NotifierOption holds information about options specific for the NotifierPlugin. type NotifierOption struct { Element ElementType `json:"element"` diff --git a/pkg/services/ngalert/notifier/legacy_storage/compat.go b/pkg/services/ngalert/notifier/legacy_storage/compat.go index e7b034d4889..ec3c8a8aec3 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/compat.go +++ b/pkg/services/ngalert/notifier/legacy_storage/compat.go @@ -124,7 +124,7 @@ func PostableGrafanaReceiversToIntegrations(postables []*apimodels.PostableGrafa } func PostableGrafanaReceiverToIntegration(p *apimodels.PostableGrafanaReceiver) (*models.Integration, error) { - config, err := models.IntegrationConfigFromType(p.Type) + config, err := models.IntegrationConfigFromType(p.Type, nil) if err != nil { return nil, err } diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index a854671376f..f38d0924a27 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -1522,14 +1522,15 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) func createIntegration(t *testing.T, integrationType string) v0alpha1.ReceiverIntegration { cfg, ok := notify.AllKnownConfigsForTesting[integrationType] require.Truef(t, ok, "no known config for integration type %s", integrationType) - return createIntegrationWithSettings(t, integrationType, cfg.Config) + return createIntegrationWithSettings(t, integrationType, "v1", cfg.Config) } -func createIntegrationWithSettings(t *testing.T, integrationType string, settingsJson string) v0alpha1.ReceiverIntegration { +func createIntegrationWithSettings(t *testing.T, integrationType string, integrationVersion string, settingsJson string) v0alpha1.ReceiverIntegration { settings := common.Unstructured{} require.NoError(t, settings.UnmarshalJSON([]byte(settingsJson))) return v0alpha1.ReceiverIntegration{ Settings: settings.Object, Type: integrationType, + Version: integrationVersion, DisableResolveMessage: util.Pointer(false), } } diff --git a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json index 569369c3733..6240ab9fd57 100644 --- a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json @@ -3412,6 +3412,7 @@ "type": "object", "required": [ "type", + "version", "settings" ], "properties": { @@ -3437,6 +3438,10 @@ }, "uid": { "type": "string" + }, + "version": { + "type": "string", + "default": "" } } },