From 372082d2540080fecb436037e243678d09cdc0c9 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 12 Oct 2023 22:33:57 +0100 Subject: [PATCH] Alerting: Export of contact points to HCL (#75849) * add compat layer to convert from Export model to "new" API models --- pkg/services/ngalert/api/api_provisioning.go | 19 +- .../ngalert/api/compat_contact_points.go | 446 ++++++++++++++++++ .../ngalert/api/compat_contact_points_test.go | 187 ++++++++ .../api/tooling/definitions/contact_points.go | 284 +++++++++++ .../export/GrafanaReceiverExporter.tsx | 4 +- .../export/GrafanaReceiversExporter.tsx | 4 +- 6 files changed, 932 insertions(+), 12 deletions(-) create mode 100644 pkg/services/ngalert/api/compat_contact_points.go create mode 100644 pkg/services/ngalert/api/compat_contact_points_test.go create mode 100644 pkg/services/ngalert/api/tooling/definitions/contact_points.go diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index af8feda9eb7..edfb8b1b248 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -548,15 +548,18 @@ func exportHcl(download bool, body definitions.AlertingFileExport) response.Resp Body: &gr, }) } + for idx, cp := range body.ContactPoints { + upd, err := ContactPointFromContactPointExport(cp) + if err != nil { + return response.Error(http.StatusInternalServerError, "failed to convert contact points to HCL", err) + } + resources = append(resources, hcl.Resource{ + Type: "grafana_contact_point", + Name: fmt.Sprintf("contact_point_%d", idx), + Body: &upd, + }) + } - // TODO implement support. - // for idx, cp := range ex.ContactPoints { - // resources = append(resources, resourceBlock{ - // Type: "grafana_contact_point", - // Name: fmt.Sprintf("contact_point_%d", idx), - // Body: &cp, - // }) - // } for idx, cp := range body.Policies { policy := cp.Policy resources = append(resources, hcl.Resource{ diff --git a/pkg/services/ngalert/api/compat_contact_points.go b/pkg/services/ngalert/api/compat_contact_points.go new file mode 100644 index 00000000000..b6afc5f83df --- /dev/null +++ b/pkg/services/ngalert/api/compat_contact_points.go @@ -0,0 +1,446 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "unsafe" + + "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/receivers" + jsoniter "github.com/json-iterator/go" + "github.com/modern-go/reflect2" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/util" +) + +// ContactPointFromContactPointExport parses the database model of the contact point (group of integrations) where settings are represented in JSON, +// to strongly typed ContactPoint. +func ContactPointFromContactPointExport(rawContactPoint definitions.ContactPointExport) (definitions.ContactPoint, error) { + j := jsoniter.ConfigCompatibleWithStandardLibrary + j.RegisterExtension(&contactPointsExtension{}) + + contactPoint := definitions.ContactPoint{ + Name: rawContactPoint.Name, + } + var errs []error + for _, rawIntegration := range rawContactPoint.Receivers { + err := parseIntegration(j, &contactPoint, rawIntegration.Type, rawIntegration.DisableResolveMessage, json.RawMessage(rawIntegration.Settings)) + if err != nil { + // accumulate errors to report all at once. + errs = append(errs, fmt.Errorf("failed to parse %s integration (uid:%s): %w", rawIntegration.Type, rawIntegration.UID, err)) + } + } + return contactPoint, errors.Join(errs...) +} + +// ContactPointToContactPointExport converts definitions.ContactPoint to notify.APIReceiver. +// It uses special extension for json-iterator API that properly handles marshalling of some specific fields. +// +//nolint:gocyclo +func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIReceiver, error) { + j := jsoniter.ConfigCompatibleWithStandardLibrary + // use json iterator with custom extension that has special codec for some field. + // This is needed to keep the API models clean and convert from database model + j.RegisterExtension(&contactPointsExtension{}) + + var integration []*notify.GrafanaIntegrationConfig + + var errs []error + for _, i := range cp.Alertmanager { + el, err := marshallIntegration(j, "prometheus-alertmanager", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Dingding { + el, err := marshallIntegration(j, "dingding", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Discord { + el, err := marshallIntegration(j, "discord", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Email { + el, err := marshallIntegration(j, "email", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Googlechat { + el, err := marshallIntegration(j, "googlechat", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Kafka { + el, err := marshallIntegration(j, "kafka", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Line { + el, err := marshallIntegration(j, "line", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Opsgenie { + el, err := marshallIntegration(j, "opsgenie", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Pagerduty { + el, err := marshallIntegration(j, "pagerduty", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.OnCall { + el, err := marshallIntegration(j, "oncall", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Pushover { + el, err := marshallIntegration(j, "pushover", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Sensugo { + el, err := marshallIntegration(j, "sensugo", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Slack { + el, err := marshallIntegration(j, "slack", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Teams { + el, err := marshallIntegration(j, "teams", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Telegram { + el, err := marshallIntegration(j, "telegram", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Threema { + el, err := marshallIntegration(j, "threema", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Victorops { + el, err := marshallIntegration(j, "victorops", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Webhook { + el, err := marshallIntegration(j, "webhook", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Wecom { + el, err := marshallIntegration(j, "wecom", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + for _, i := range cp.Webex { + el, err := marshallIntegration(j, "webex", i, i.DisableResolveMessage) + integration = append(integration, el) + if err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return notify.APIReceiver{}, errors.Join(errs...) + } + contactPoint := notify.APIReceiver{ + ConfigReceiver: notify.ConfigReceiver{Name: cp.Name}, + GrafanaIntegrations: notify.GrafanaIntegrations{Integrations: integration}, + } + return contactPoint, nil +} + +// marshallIntegration converts the API model integration to the storage model that contains settings in the JSON format. +// The secret fields are not encrypted. +func marshallIntegration(json jsoniter.API, integrationType string, integration interface{}, disableResolveMessage *bool) (*notify.GrafanaIntegrationConfig, error) { + data, err := json.Marshal(integration) + if err != nil { + return nil, fmt.Errorf("failed to marshall integration '%s' to JSON: %w", integrationType, err) + } + e := ¬ify.GrafanaIntegrationConfig{ + Type: integrationType, + Settings: data, + } + if disableResolveMessage != nil { + e.DisableResolveMessage = *disableResolveMessage + } + return e, nil +} + +//nolint:gocyclo +func parseIntegration(json jsoniter.API, result *definitions.ContactPoint, receiverType string, disableResolveMessage bool, data json.RawMessage) error { + var err error + var disable *bool + if disableResolveMessage { // populate only if true + disable = util.Pointer(disableResolveMessage) + } + switch strings.ToLower(receiverType) { + case "prometheus-alertmanager": + integration := definitions.AlertmanagerIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Alertmanager = append(result.Alertmanager, integration) + } + case "dingding": + integration := definitions.DingdingIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Dingding = append(result.Dingding, integration) + } + case "discord": + integration := definitions.DiscordIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Discord = append(result.Discord, integration) + } + case "email": + integration := definitions.EmailIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Email = append(result.Email, integration) + } + case "googlechat": + integration := definitions.GooglechatIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Googlechat = append(result.Googlechat, integration) + } + case "kafka": + integration := definitions.KafkaIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Kafka = append(result.Kafka, integration) + } + case "line": + integration := definitions.LineIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Line = append(result.Line, integration) + } + case "opsgenie": + integration := definitions.OpsgenieIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Opsgenie = append(result.Opsgenie, integration) + } + case "pagerduty": + integration := definitions.PagerdutyIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Pagerduty = append(result.Pagerduty, integration) + } + case "oncall": + integration := definitions.OnCallIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.OnCall = append(result.OnCall, integration) + } + case "pushover": + integration := definitions.PushoverIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Pushover = append(result.Pushover, integration) + } + case "sensugo": + integration := definitions.SensugoIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Sensugo = append(result.Sensugo, integration) + } + case "slack": + integration := definitions.SlackIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Slack = append(result.Slack, integration) + } + case "teams": + integration := definitions.TeamsIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Teams = append(result.Teams, integration) + } + case "telegram": + integration := definitions.TelegramIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Telegram = append(result.Telegram, integration) + } + case "threema": + integration := definitions.ThreemaIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Threema = append(result.Threema, integration) + } + case "victorops": + integration := definitions.VictoropsIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Victorops = append(result.Victorops, integration) + } + case "webhook": + integration := definitions.WebhookIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Webhook = append(result.Webhook, integration) + } + case "wecom": + integration := definitions.WecomIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Wecom = append(result.Wecom, integration) + } + case "webex": + integration := definitions.WebexIntegration{DisableResolveMessage: disable} + if err = json.Unmarshal(data, &integration); err == nil { + result.Webex = append(result.Webex, integration) + } + default: + err = fmt.Errorf("integration %s is not supported", receiverType) + } + return err +} + +// contactPointsExtension extends jsoniter with special codecs for some integrations' fields that are encoded differently in the legacy configuration. +type contactPointsExtension struct { + jsoniter.DummyExtension +} + +func (c contactPointsExtension) UpdateStructDescriptor(structDescriptor *jsoniter.StructDescriptor) { + if structDescriptor.Type == reflect2.TypeOf(definitions.EmailIntegration{}) { + bind := structDescriptor.GetField("Addresses") + codec := &emailAddressCodec{} + bind.Decoder = codec + bind.Encoder = codec + } + if structDescriptor.Type == reflect2.TypeOf(definitions.PushoverIntegration{}) { + codec := &numberAsStringCodec{} + for _, field := range []string{"AlertingPriority", "OKPriority"} { + desc := structDescriptor.GetField(field) + desc.Decoder = codec + desc.Encoder = codec + } + // the same logic is in the pushover.NewConfig in alerting module + codec = &numberAsStringCodec{ignoreError: true} + for _, field := range []string{"Retry", "Expire"} { + desc := structDescriptor.GetField(field) + desc.Decoder = codec + desc.Encoder = codec + } + } + if structDescriptor.Type == reflect2.TypeOf(definitions.WebhookIntegration{}) { + codec := &numberAsStringCodec{ignoreError: true} + desc := structDescriptor.GetField("MaxAlerts") + desc.Decoder = codec + desc.Encoder = codec + } + if structDescriptor.Type == reflect2.TypeOf(definitions.OnCallIntegration{}) { + codec := &numberAsStringCodec{ignoreError: true} + desc := structDescriptor.GetField("MaxAlerts") + desc.Decoder = codec + desc.Encoder = codec + } +} + +type emailAddressCodec struct{} + +func (d *emailAddressCodec) IsEmpty(ptr unsafe.Pointer) bool { + f := *(*[]string)(ptr) + return len(f) == 0 +} + +func (d *emailAddressCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { + f := *(*[]string)(ptr) + addresses := strings.Join(f, ";") + stream.WriteString(addresses) +} + +func (d *emailAddressCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + s := iter.ReadString() + emails := strings.FieldsFunc(strings.Trim(s, "\""), func(r rune) bool { + switch r { + case ',', ';', '\n': + return true + } + return false + }) + *((*[]string)(ptr)) = emails +} + +// converts a string representation of a number to *int64 +type numberAsStringCodec struct { + ignoreError bool // if true, then ignores the error and keeps value nil +} + +func (d *numberAsStringCodec) IsEmpty(ptr unsafe.Pointer) bool { + return *((*(*int))(ptr)) == nil +} + +func (d *numberAsStringCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { + val := *((*(*int))(ptr)) + if val == nil { + stream.WriteNil() + return + } + stream.WriteInt(*val) +} + +func (d *numberAsStringCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + valueType := iter.WhatIsNext() + var value int64 + switch valueType { + case jsoniter.NumberValue: + value = iter.ReadInt64() + case jsoniter.StringValue: + var num receivers.OptionalNumber + err := num.UnmarshalJSON(iter.ReadStringAsSlice()) + if err != nil { + iter.ReportError("numberAsStringCodec", fmt.Sprintf("failed to unmarshall string as OptionalNumber: %s", err.Error())) + } + if num.String() == "" { + return + } + value, err = num.Int64() + if err != nil { + if !d.ignoreError { + iter.ReportError("numberAsStringCodec", fmt.Sprintf("string does not represent an integer number: %s", err.Error())) + } + return + } + case jsoniter.NilValue: + iter.ReadNil() + return + default: + iter.ReportError("numberAsStringCodec", "not number or string") + } + *((*(*int64))(ptr)) = &value +} diff --git a/pkg/services/ngalert/api/compat_contact_points_test.go b/pkg/services/ngalert/api/compat_contact_points_test.go new file mode 100644 index 00000000000..8700212edba --- /dev/null +++ b/pkg/services/ngalert/api/compat_contact_points_test.go @@ -0,0 +1,187 @@ +package api + +import ( + "context" + "encoding/base64" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/alerting/notify" + receiversTesting "github.com/grafana/alerting/receivers/testing" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" +) + +// Test that conversion notify.APIReceiver -> definitions.ContactPoint -> notify.APIReceiver does not lose data +func TestContactPointFromContactPointExports(t *testing.T) { + getContactPointExport := func(t *testing.T, receiver *notify.APIReceiver) definitions.ContactPointExport { + export := make([]definitions.ReceiverExport, 0, len(receiver.Integrations)) + for _, integrationConfig := range receiver.Integrations { + postable := &definitions.PostableGrafanaReceiver{ + UID: integrationConfig.UID, + Name: integrationConfig.Name, + Type: integrationConfig.Type, + DisableResolveMessage: integrationConfig.DisableResolveMessage, + Settings: definitions.RawMessage(integrationConfig.Settings), + SecureSettings: integrationConfig.SecureSettings, + } + emb, err := provisioning.PostableGrafanaReceiverToEmbeddedContactPoint( + postable, + models.ProvenanceNone, + func(s string) string { // test configs are not encrypted but encoded + d, err := base64.StdEncoding.DecodeString(s) + require.NoError(t, err) + return string(d) + }) + require.NoError(t, err) + ex, err := ReceiverExportFromEmbeddedContactPoint(emb) + require.NoError(t, err) + export = append(export, ex) + } + + return definitions.ContactPointExport{ + OrgID: 1, + Name: receiver.Name, + Receivers: export, + } + } + + // use the configs for testing because they have all fields supported by integrations + for integrationType, cfg := range notify.AllKnownConfigsForTesting { + t.Run(integrationType, func(t *testing.T) { + recCfg := ¬ify.APIReceiver{ + ConfigReceiver: notify.ConfigReceiver{Name: "test-receiver"}, + GrafanaIntegrations: notify.GrafanaIntegrations{ + Integrations: []*notify.GrafanaIntegrationConfig{ + cfg.GetRawNotifierConfig("test"), + }, + }, + } + + expected, err := notify.BuildReceiverConfiguration(context.Background(), recCfg, func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string { + return receiversTesting.DecryptForTesting(sjd)(key, fallback) + }) + require.NoError(t, err) + + result, err := ContactPointFromContactPointExport(getContactPointExport(t, recCfg)) + require.NoError(t, err) + + back, err := ContactPointToContactPointExport(result) + require.NoError(t, err) + + actual, err := notify.BuildReceiverConfiguration(context.Background(), &back, func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string { + return receiversTesting.DecryptForTesting(sjd)(key, fallback) + }) + require.NoError(t, err) + + diff := cmp.Diff(expected, actual, cmp.FilterPath(func(path cmp.Path) bool { + return strings.Contains(path.String(), "Metadata.UID") || + strings.Contains(path.String(), "Metadata.Name") || + strings.Contains(path.String(), "WecomConfigs.Settings.EndpointURL") // This field is not exposed to user + }, cmp.Ignore())) + if len(diff) != 0 { + require.Failf(t, "The re-marshalled configuration does not match the expected one", diff) + } + }) + } + + t.Run("pushover optional numbers as string", func(t *testing.T) { + export := definitions.ContactPointExport{ + Name: "test", + Receivers: []definitions.ReceiverExport{ + { + Type: "pushover", + Settings: definitions.RawMessage( + `{ + "priority": 1, + "okPriority": "2", + "expire": null, + "retry": "invalid" + }`), + }, + }, + } + result, err := ContactPointFromContactPointExport(export) + require.NoError(t, err) + require.Len(t, result.Pushover, 1) + require.Equal(t, int64(1), *result.Pushover[0].AlertingPriority) + require.Equal(t, int64(2), *result.Pushover[0].OKPriority) + require.Nil(t, result.Pushover[0].Expire) + require.Nil(t, result.Pushover[0].Retry) + }) + t.Run("email with multiple addresses", func(t *testing.T) { + export := definitions.ContactPointExport{ + Name: "test", + Receivers: []definitions.ReceiverExport{ + { + Type: "email", + Settings: definitions.RawMessage(`{"addresses": "test@grafana.com,test2@grafana.com;test3@grafana.com\ntest4@granafa.com"}`), + }, + }, + } + result, err := ContactPointFromContactPointExport(export) + require.NoError(t, err) + require.Len(t, result.Email, 1) + require.EqualValues(t, []string{ + "test@grafana.com", + "test2@grafana.com", + "test3@grafana.com", + "test4@granafa.com", + }, result.Email[0].Addresses) + }) + t.Run("webhook with optional numbers as string", func(t *testing.T) { + export := definitions.ContactPointExport{ + Name: "test", + Receivers: []definitions.ReceiverExport{ + { + Type: "webhook", + Settings: definitions.RawMessage(`{ "maxAlerts" : "112" }`), + }, + { + Type: "webhook", + Settings: definitions.RawMessage(`{ "maxAlerts" : "test" }`), + }, + { + Type: "webhook", + Settings: definitions.RawMessage(`{ "maxAlerts" : null }`), + }, + }, + } + result, err := ContactPointFromContactPointExport(export) + require.NoError(t, err) + require.Len(t, result.Webhook, 3) + require.Equal(t, int64(112), *result.Webhook[0].MaxAlerts) + require.Nil(t, result.Webhook[1].MaxAlerts) + require.Nil(t, result.Webhook[2].MaxAlerts) + }) + + t.Run("oncall with optional numbers as string", func(t *testing.T) { + export := definitions.ContactPointExport{ + Name: "test", + Receivers: []definitions.ReceiverExport{ + { + Type: "oncall", + Settings: definitions.RawMessage(`{ "maxAlerts" : "112" }`), + }, + { + Type: "oncall", + Settings: definitions.RawMessage(`{ "maxAlerts" : "test" }`), + }, + { + Type: "oncall", + Settings: definitions.RawMessage(`{ "maxAlerts" : null }`), + }, + }, + } + result, err := ContactPointFromContactPointExport(export) + require.NoError(t, err) + require.Len(t, result.OnCall, 3) + require.Equal(t, int64(112), *result.OnCall[0].MaxAlerts) + require.Nil(t, result.OnCall[1].MaxAlerts) + require.Nil(t, result.OnCall[2].MaxAlerts) + }) +} diff --git a/pkg/services/ngalert/api/tooling/definitions/contact_points.go b/pkg/services/ngalert/api/tooling/definitions/contact_points.go new file mode 100644 index 00000000000..3f484be097f --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/contact_points.go @@ -0,0 +1,284 @@ +package definitions + +// This file contains API models of integrations that are supported by Grafana Managed Alerts. +// The models below match the Config models described in the module github.com/grafana/alerting, package 'receivers/**' +// as well as models described in Grafana Terraform Provider. +// Currently, they are used only for export to HCL but in the future we expand their scope. +// The consistency between models in the alerting module and this file is enforced by unit-tests. + +// +// 1. JSON tags are used for unmarshalling from the definitions.PostableGrafanaReceiver.Settings. +// 2. YAML tags are not used but kept while copying of models from the alerting module +// 3. Each integration struct contains field 'DisableResolveMessage'. In Terraform provider the field is on the same level as the settings. +// Currently, HCL encoder does not support composition of structures or generic ones. This can be change after https://github.com/hashicorp/hcl/issues/290 is solved. +// 4. Sensitive fields have type Secret. Currently, this is done for information purpose and is not used anywhere. + +// A string that contain sensitive information. +type Secret string // TODO implement masking fields when models are used + +type AlertmanagerIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` // TODO change when https://github.com/hashicorp/hcl/issues/290 is fixed + + URL string `json:"url" yaml:"url" hcl:"url"` + User *string `json:"basicAuthUser,omitempty" yaml:"basicAuthUser,omitempty" hcl:"basic_auth_user"` + Password *Secret `json:"basicAuthPassword,omitempty" yaml:"basicAuthPassword,omitempty" hcl:"basic_auth_password"` +} + +type DingdingIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL string `json:"url,omitempty" yaml:"url,omitempty" hcl:"url"` + MessageType *string `json:"msgType,omitempty" yaml:"msgType,omitempty" hcl:"message_type"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` +} + +type DiscordIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + WebhookURL Secret `json:"url" yaml:"url" hcl:"url"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + AvatarURL *string `json:"avatar_url,omitempty" yaml:"avatar_url,omitempty" hcl:"avatar_url"` + UseDiscordUsername *bool `json:"use_discord_username,omitempty" yaml:"use_discord_username,omitempty" hcl:"use_discord_username"` +} + +type EmailIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + Addresses []string `json:"addresses" yaml:"addresses" hcl:"addresses"` + + SingleEmail *bool `json:"singleEmail,omitempty" yaml:"singleEmail,omitempty" hcl:"single_email"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + Subject *string `json:"subject,omitempty" yaml:"subject,omitempty" hcl:"subject"` +} + +type GooglechatIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL string `json:"url" yaml:"url" hcl:"url"` + + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` +} + +type KafkaIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + Endpoint Secret `json:"kafkaRestProxy" yaml:"kafkaRestProxy" hcl:"rest_proxy_url"` + Topic string `json:"kafkaTopic" yaml:"kafkaTopic" hcl:"topic"` + + Description *string `json:"description,omitempty" yaml:"description,omitempty" hcl:"description"` + Details *string `json:"details,omitempty" yaml:"details,omitempty" hcl:"details"` + Username *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"username"` + Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"password"` + APIVersion *string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty" hcl:"api_version"` + KafkaClusterID *string `json:"kafkaClusterId,omitempty" yaml:"kafkaClusterId,omitempty" hcl:"cluster_id"` +} + +type LineIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + Token Secret `json:"token" yaml:"token" hcl:"token"` + + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Description *string `json:"description,omitempty" yaml:"description,omitempty" hcl:"description"` +} + +type OnCallIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL string `json:"url" yaml:"url" hcl:"url"` + + HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"` + MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"` + AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"` + AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"` + User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"` + Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` +} + +type OpsgenieIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + APIKey Secret `json:"apiKey" yaml:"apiKey" hcl:"api_key"` + + APIUrl *string `json:"apiUrl,omitempty" yaml:"apiUrl,omitempty" hcl:"url"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + Description *string `json:"description,omitempty" yaml:"description,omitempty" hcl:"description"` + AutoClose *bool `json:"autoClose,omitempty" yaml:"autoClose,omitempty" hcl:"auto_close"` + OverridePriority *bool `json:"overridePriority,omitempty" yaml:"overridePriority,omitempty" hcl:"override_priority"` + SendTagsAs *string `json:"sendTagsAs,omitempty" yaml:"sendTagsAs,omitempty" hcl:"send_tags_as"` +} + +type PagerdutyIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + Key Secret `json:"integrationKey" yaml:"integrationKey" hcl:"integration_key"` + + Severity *string `json:"severity,omitempty" yaml:"severity,omitempty" hcl:"severity"` + Class *string `json:"class,omitempty" yaml:"class,omitempty" hcl:"class"` + Component *string `json:"component,omitempty" yaml:"component,omitempty" hcl:"component"` + Group *string `json:"group,omitempty" yaml:"group,omitempty" hcl:"group"` + Summary *string `json:"summary,omitempty" yaml:"summary,omitempty" hcl:"summary"` + Source *string `json:"source,omitempty" yaml:"source,omitempty" hcl:"source"` + Client *string `json:"client,omitempty" yaml:"client,omitempty" hcl:"client"` + ClientURL *string `json:"client_url,omitempty" yaml:"client_url,omitempty" hcl:"client_url"` + Details *map[string]string `json:"details,omitempty" yaml:"details,omitempty" hcl:"details"` +} + +type PushoverIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + UserKey Secret `json:"userKey" yaml:"userKey" hcl:"user_key"` + APIToken Secret `json:"apiToken" yaml:"apiToken" hcl:"api_token"` + + AlertingPriority *int64 `json:"priority,omitempty" yaml:"priority,omitempty" hcl:"priority"` + OKPriority *int64 `json:"okPriority,omitempty" yaml:"okPriority,omitempty" hcl:"ok_priority"` + Retry *int64 `json:"retry,omitempty" yaml:"retry,omitempty" hcl:"retry"` + Expire *int64 `json:"expire,omitempty" yaml:"expire,omitempty" hcl:"expire"` + Device *string `json:"device,omitempty" yaml:"device,omitempty" hcl:"device"` + AlertingSound *string `json:"sound,omitempty" yaml:"sound,omitempty" hcl:"sound"` + OKSound *string `json:"okSound,omitempty" yaml:"okSound,omitempty" hcl:"ok_sound"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + UploadImage *bool `json:"uploadImage,omitempty" yaml:"uploadImage,omitempty" hcl:"upload_image"` +} + +type SensugoIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL string `json:"url" yaml:"url" hcl:"url"` + APIKey Secret `json:"apikey" yaml:"apikey" hcl:"api_key"` + + Entity *string `json:"entity,omitempty" yaml:"entity,omitempty" hcl:"entity"` + Check *string `json:"check,omitempty" yaml:"check,omitempty" hcl:"check"` + Namespace *string `json:"namespace,omitempty" yaml:"namespace,omitempty" hcl:"namespace"` + Handler *string `json:"handler,omitempty" yaml:"handler,omitempty" hcl:"handler"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` +} + +type SlackIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + EndpointURL *string `json:"endpointUrl,omitempty" yaml:"endpointUrl,omitempty" hcl:"endpoint_url"` + URL *Secret `json:"url,omitempty" yaml:"url,omitempty" hcl:"url"` + Token *Secret `json:"token,omitempty" yaml:"token,omitempty" hcl:"token"` + Recipient *string `json:"recipient,omitempty" yaml:"recipient,omitempty" hcl:"recipient"` + Text *string `json:"text,omitempty" yaml:"text,omitempty" hcl:"text"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Username *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"username"` + IconEmoji *string `json:"icon_emoji,omitempty" yaml:"icon_emoji,omitempty" hcl:"icon_emoji"` + IconURL *string `json:"icon_url,omitempty" yaml:"icon_url,omitempty" hcl:"icon_url"` + MentionChannel *string `json:"mentionChannel,omitempty" yaml:"mentionChannel,omitempty" hcl:"mention_channel"` + MentionUsers *string `json:"mentionUsers,omitempty" yaml:"mentionUsers,omitempty" hcl:"mention_users"` + MentionGroups *string `json:"mentionGroups,omitempty" yaml:"mentionGroups,omitempty" hcl:"mention_groups"` +} + +type TelegramIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + BotToken Secret `json:"bottoken" yaml:"bottoken" hcl:"token"` + ChatID string `json:"chatid,omitempty" yaml:"chatid,omitempty" hcl:"chat_id"` + + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + ParseMode *string `json:"parse_mode,omitempty" yaml:"parse_mode,omitempty" hcl:"parse_mode"` + DisableWebPagePreview *bool `json:"disable_web_page_preview,omitempty" yaml:"disable_web_page_preview,omitempty" hcl:"disable_web_page_preview"` + ProtectContent *bool `json:"protect_content,omitempty" yaml:"protect_content,omitempty" hcl:"protect_content"` + DisableNotifications *bool `json:"disable_notifications,omitempty" yaml:"disable_notifications,omitempty" hcl:"disable_notifications"` +} + +type TeamsIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL Secret `json:"url,omitempty" yaml:"url,omitempty" hcl:"url"` + + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + SectionTitle *string `json:"sectiontitle,omitempty" yaml:"sectiontitle,omitempty" hcl:"section_title"` +} + +type ThreemaIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + GatewayID string `json:"gateway_id" yaml:"gateway_id" hcl:"gateway_id"` + RecipientID string `json:"recipient_id" yaml:"recipient_id" hcl:"recipient_id"` + APISecret Secret `json:"api_secret" yaml:"api_secret" hcl:"api_secret"` + + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Description *string `json:"description,omitempty" yaml:"description,omitempty" hcl:"description"` +} + +type VictoropsIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL string `json:"url" yaml:"url" hcl:"url"` + + MessageType *string `json:"messageType,omitempty" yaml:"messageType,omitempty" hcl:"message_type"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Description *string `json:"description,omitempty" yaml:"description,omitempty" hcl:"description"` +} + +type WebexIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + Token Secret `json:"bot_token" yaml:"bot_token" hcl:"token"` + + APIURL *string `json:"api_url,omitempty" yaml:"api_url,omitempty" hcl:"api_url"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + RoomID *string `json:"room_id,omitempty" yaml:"room_id,omitempty" hcl:"room_id"` +} + +type WebhookIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL string `json:"url" yaml:"url" hcl:"url"` + + HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"` + MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"` + AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"` + AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"` + User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"` + Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` +} + +type WecomIntegration struct { + DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"` + + URL *Secret `json:"url,omitempty" yaml:"url,omitempty" hcl:"url"` + Secret *Secret `json:"secret,omitempty" yaml:"secret,omitempty" hcl:"secret"` + AgentID *string `json:"agent_id,omitempty" yaml:"agent_id,omitempty" hcl:"agent_id"` + CorpID *string `json:"corp_id,omitempty" yaml:"corp_id,omitempty" hcl:"corp_id"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + MsgType *string `json:"msgtype,omitempty" yaml:"msgtype,omitempty" hcl:"msg_type"` + ToUser *string `json:"touser,omitempty" yaml:"touser,omitempty" hcl:"to_user"` +} + +type ContactPoint struct { + Name string `json:"name" yaml:"name" hcl:"name"` + Alertmanager []AlertmanagerIntegration `json:"alertmanager" yaml:"alertmanager" hcl:"alertmanager,block"` + Dingding []DingdingIntegration `json:"dingding" yaml:"dingding" hcl:"dingding,block"` + Discord []DiscordIntegration `json:"discord" yaml:"discord" hcl:"discord,block"` + Email []EmailIntegration `json:"email" yaml:"email" hcl:"email,block"` + Googlechat []GooglechatIntegration `json:"googlechat" yaml:"googlechat" hcl:"googlechat,block"` + Kafka []KafkaIntegration `json:"kafka" yaml:"kafka" hcl:"kafka,block"` + Line []LineIntegration `json:"line" yaml:"line" hcl:"line,block"` + Opsgenie []OpsgenieIntegration `json:"opsgenie" yaml:"opsgenie" hcl:"opsgenie,block"` + Pagerduty []PagerdutyIntegration `json:"pagerduty" yaml:"pagerduty" hcl:"pagerduty,block"` + OnCall []OnCallIntegration `json:"oncall" yaml:"oncall" hcl:"oncall,block"` + Pushover []PushoverIntegration `json:"pushover" yaml:"pushover" hcl:"pushover,block"` + Sensugo []SensugoIntegration `json:"sensugo" yaml:"sensugo" hcl:"sensugo,block"` + Slack []SlackIntegration `json:"slack" yaml:"slack" hcl:"slack,block"` + Teams []TeamsIntegration `json:"teams" yaml:"teams" hcl:"teams,block"` + Telegram []TelegramIntegration `json:"telegram" yaml:"telegram" hcl:"telegram,block"` + Threema []ThreemaIntegration `json:"threema" yaml:"threema" hcl:"threema,block"` + Victorops []VictoropsIntegration `json:"victorops" yaml:"victorops" hcl:"victorops,block"` + Webhook []WebhookIntegration `json:"webhook" yaml:"webhook" hcl:"webhook,block"` + Wecom []WecomIntegration `json:"wecom" yaml:"wecom" hcl:"wecom,block"` + Webex []WebexIntegration `json:"webex" yaml:"webex" hcl:"webex,block"` +} diff --git a/public/app/features/alerting/unified/components/export/GrafanaReceiverExporter.tsx b/public/app/features/alerting/unified/components/export/GrafanaReceiverExporter.tsx index ddf4380ac92..6a90aca6918 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaReceiverExporter.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaReceiverExporter.tsx @@ -6,7 +6,7 @@ import { alertRuleApi } from '../../api/alertRuleApi'; import { FileExportPreview } from './FileExportPreview'; import { GrafanaExportDrawer } from './GrafanaExportDrawer'; -import { ExportFormats, jsonAndYamlGrafanaExportProviders } from './providers'; +import { allGrafanaExportProviders, ExportFormats } from './providers'; interface GrafanaReceiverExportPreviewProps { exportFormat: ExportFormats; @@ -57,7 +57,7 @@ export const GrafanaReceiverExporter = ({ onClose, receiverName, decrypt }: Graf activeTab={activeTab} onTabChange={setActiveTab} onClose={onClose} - formatProviders={jsonAndYamlGrafanaExportProviders} + formatProviders={Object.values(allGrafanaExportProviders)} >