mirror of https://github.com/grafana/grafana
Alerting: Export of contact points to HCL (#75849)
* add compat layer to convert from Export model to "new" API modelspull/76492/head^2
parent
0a6d78f35e
commit
372082d254
@ -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 |
||||
} |
@ -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) |
||||
}) |
||||
} |
@ -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"` |
||||
} |
Loading…
Reference in new issue