mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
372082d254
* add compat layer to convert from Export model to "new" API models
447 lines
15 KiB
Go
447 lines
15 KiB
Go
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
|
|
}
|