Alerting: Update integration schema to support versions (#109969)
* add VersionedNotifierPlugin and method that converts NotifierPlugin to it * return new schema if query parameter version=2 * add version to k8s model of integration * fix open api snapshot * add version to IntegrationConfig * use current version on conversion * create versioned integrations for test
This commit is contained in:
@@ -8,6 +8,7 @@ ReceiverSpec: {
|
||||
#Integration: {
|
||||
uid?: string
|
||||
type: string
|
||||
version: string
|
||||
disableResolveMessage?: bool
|
||||
settings: {
|
||||
[string]: _
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+5
-1
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3412,6 +3412,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"version",
|
||||
"settings"
|
||||
],
|
||||
"properties": {
|
||||
@@ -3437,6 +3438,10 @@
|
||||
},
|
||||
"uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user