From e5dbd23c64c1e446a16b5e1be341687855c285f9 Mon Sep 17 00:00:00 2001 From: Gentleelephant Date: Tue, 20 Dec 2022 14:33:16 +0800 Subject: [PATCH] support discord Signed-off-by: Gentleelephant support discord Signed-off-by: Gentleelephant --- config/bundle.yaml | 123 +++++++++++ ...on.kubesphere.io_notificationmanagers.yaml | 9 + .../notification.kubesphere.io_receivers.yaml | 114 ++++++++++ helm/crds/bundle.yaml | 123 +++++++++++ pkg/apis/v2beta2/notificationmanager_types.go | 8 + pkg/apis/v2beta2/receiver_types.go | 25 +++ pkg/apis/v2beta2/receiver_webhook.go | 29 +++ pkg/apis/v2beta2/zz_generated.deepcopy.go | 85 ++++++++ pkg/constants/constants.go | 4 + pkg/controller/factories.go | 3 + pkg/internal/discord/types.go | 88 ++++++++ pkg/notify/notifier/discord/discord.go | 200 ++++++++++++++++++ pkg/notify/notify.go | 2 + 13 files changed, 813 insertions(+) create mode 100644 pkg/internal/discord/types.go create mode 100644 pkg/notify/notifier/discord/discord.go diff --git a/config/bundle.yaml b/config/bundle.yaml index 070f5862..ba27e310 100644 --- a/config/bundle.yaml +++ b/config/bundle.yaml @@ -5432,6 +5432,15 @@ spec: format: int64 type: integer type: object + discord: + properties: + notificationTimeout: + description: Notification Sending Timeout + format: int32 + type: integer + template: + type: string + type: object email: properties: deliveryType: @@ -9434,6 +9443,120 @@ spec: description: 'template type: text or markdown' type: string type: object + discord: + properties: + alertSelector: + description: Selector to filter alerts. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If + the operator is In or NotIn, the values array must + be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A + single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is "key", + the operator is "In", and the values array contains only + "value". The requirements are ANDed. + type: object + type: object + enabled: + description: whether the receiver is enabled + type: boolean + mentionedRoles: + description: Mentioned roles + items: + type: string + type: array + mentionedUsers: + description: Mentioned users + items: + type: string + type: array + template: + type: string + tmplText: + description: Template file. + properties: + key: + description: The key of the configmap to select from. Must + be a valid configmap key. + type: string + name: + description: Name of the configmap. + type: string + namespace: + description: The namespace of the configmap, default to the + `defaultSecretNamespace` of `NotificationManager` crd. If + the `defaultSecretNamespace` does not set, default to the + pod's namespace. + type: string + required: + - name + type: object + type: + description: content or embed + type: string + webhook: + properties: + value: + type: string + valueFrom: + properties: + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: The namespace of the secret, default + to the `defaultSecretNamespace` of `NotificationManager` + crd. If the `defaultSecretNamespace` does not set, + default to the pod's namespace. + type: string + required: + - key + - name + type: object + type: object + type: object + required: + - webhook + type: object email: properties: alertSelector: diff --git a/config/crd/bases/notification.kubesphere.io_notificationmanagers.yaml b/config/crd/bases/notification.kubesphere.io_notificationmanagers.yaml index 8d19b11d..bbaa724d 100644 --- a/config/crd/bases/notification.kubesphere.io_notificationmanagers.yaml +++ b/config/crd/bases/notification.kubesphere.io_notificationmanagers.yaml @@ -4292,6 +4292,15 @@ spec: format: int64 type: integer type: object + discord: + properties: + notificationTimeout: + description: Notification Sending Timeout + format: int32 + type: integer + template: + type: string + type: object email: properties: deliveryType: diff --git a/config/crd/bases/notification.kubesphere.io_receivers.yaml b/config/crd/bases/notification.kubesphere.io_receivers.yaml index 4923010a..86af38b3 100644 --- a/config/crd/bases/notification.kubesphere.io_receivers.yaml +++ b/config/crd/bases/notification.kubesphere.io_receivers.yaml @@ -1024,6 +1024,120 @@ spec: description: 'template type: text or markdown' type: string type: object + discord: + properties: + alertSelector: + description: Selector to filter alerts. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If + the operator is In or NotIn, the values array must + be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A + single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is "key", + the operator is "In", and the values array contains only + "value". The requirements are ANDed. + type: object + type: object + enabled: + description: whether the receiver is enabled + type: boolean + mentionedRoles: + description: Mentioned roles + items: + type: string + type: array + mentionedUsers: + description: Mentioned users + items: + type: string + type: array + template: + type: string + tmplText: + description: Template file. + properties: + key: + description: The key of the configmap to select from. Must + be a valid configmap key. + type: string + name: + description: Name of the configmap. + type: string + namespace: + description: The namespace of the configmap, default to the + `defaultSecretNamespace` of `NotificationManager` crd. If + the `defaultSecretNamespace` does not set, default to the + pod's namespace. + type: string + required: + - name + type: object + type: + description: content or embed + type: string + webhook: + properties: + value: + type: string + valueFrom: + properties: + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: The namespace of the secret, default + to the `defaultSecretNamespace` of `NotificationManager` + crd. If the `defaultSecretNamespace` does not set, + default to the pod's namespace. + type: string + required: + - key + - name + type: object + type: object + type: object + required: + - webhook + type: object email: properties: alertSelector: diff --git a/helm/crds/bundle.yaml b/helm/crds/bundle.yaml index 893052dd..085b3be3 100644 --- a/helm/crds/bundle.yaml +++ b/helm/crds/bundle.yaml @@ -5432,6 +5432,15 @@ spec: format: int64 type: integer type: object + discord: + properties: + notificationTimeout: + description: Notification Sending Timeout + format: int32 + type: integer + template: + type: string + type: object email: properties: deliveryType: @@ -9434,6 +9443,120 @@ spec: description: 'template type: text or markdown' type: string type: object + discord: + properties: + alertSelector: + description: Selector to filter alerts. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If + the operator is In or NotIn, the values array must + be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A + single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is "key", + the operator is "In", and the values array contains only + "value". The requirements are ANDed. + type: object + type: object + enabled: + description: whether the receiver is enabled + type: boolean + mentionedRoles: + description: Mentioned roles + items: + type: string + type: array + mentionedUsers: + description: Mentioned users + items: + type: string + type: array + template: + type: string + tmplText: + description: Template file. + properties: + key: + description: The key of the configmap to select from. Must + be a valid configmap key. + type: string + name: + description: Name of the configmap. + type: string + namespace: + description: The namespace of the configmap, default to the + `defaultSecretNamespace` of `NotificationManager` crd. If + the `defaultSecretNamespace` does not set, default to the + pod's namespace. + type: string + required: + - name + type: object + type: + description: content or embed + type: string + webhook: + properties: + value: + type: string + valueFrom: + properties: + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: The namespace of the secret, default + to the `defaultSecretNamespace` of `NotificationManager` + crd. If the `defaultSecretNamespace` does not set, + default to the pod's namespace. + type: string + required: + - key + - name + type: object + type: object + type: object + required: + - webhook + type: object email: properties: alertSelector: diff --git a/pkg/apis/v2beta2/notificationmanager_types.go b/pkg/apis/v2beta2/notificationmanager_types.go index f76a0a96..e37f48a7 100644 --- a/pkg/apis/v2beta2/notificationmanager_types.go +++ b/pkg/apis/v2beta2/notificationmanager_types.go @@ -299,6 +299,13 @@ type FeishuOptions struct { TokenExpires time.Duration `json:"tokenExpires,omitempty"` } +type DiscordOptions struct { + // Notification Sending Timeout + NotificationTimeout *int32 `json:"notificationTimeout,omitempty"` + + Template string `json:"template,omitempty"` +} + type Options struct { Global *GlobalOptions `json:"global,omitempty"` Email *EmailOptions `json:"email,omitempty"` @@ -309,6 +316,7 @@ type Options struct { Sms *SmsOptions `json:"sms,omitempty"` Pushover *PushoverOptions `json:"pushover,omitempty"` Feishu *FeishuOptions `json:"feishu,omitempty"` + Discord *DiscordOptions `json:"discord,omitempty"` } // NotificationManagerStatus defines the observed state of NotificationManager diff --git a/pkg/apis/v2beta2/receiver_types.go b/pkg/apis/v2beta2/receiver_types.go index 566acc2c..3d0a41a5 100644 --- a/pkg/apis/v2beta2/receiver_types.go +++ b/pkg/apis/v2beta2/receiver_types.go @@ -207,6 +207,30 @@ type WechatChatBot struct { AtMobiles []string `json:"atMobiles,omitempty"` } +type DiscordReceiver struct { + + // whether the receiver is enabled + Enabled *bool `json:"enabled,omitempty"` + + Webhook *Credential `json:"webhook"` + + Template *string `json:"template,omitempty"` + // Template file. + TmplText *ConfigmapKeySelector `json:"tmplText,omitempty"` + + // content or embed + Type *string `json:"type,omitempty"` + + // Mentioned users + MentionedUsers []string `json:"mentionedUsers,omitempty"` + + // Mentioned roles + MentionedRoles []string `json:"mentionedRoles,omitempty"` + + // Selector to filter alerts. + AlertSelector *metav1.LabelSelector `json:"alertSelector,omitempty"` +} + type SmsReceiver struct { // whether the receiver is enabled Enabled *bool `json:"enabled,omitempty"` @@ -302,6 +326,7 @@ type ReceiverSpec struct { Sms *SmsReceiver `json:"sms,omitempty"` Pushover *PushoverReceiver `json:"pushover,omitempty"` Feishu *FeishuReceiver `json:"feishu,omitempty"` + Discord *DiscordReceiver `json:"discord,omitempty"` } // ReceiverStatus defines the observed state of Receiver diff --git a/pkg/apis/v2beta2/receiver_webhook.go b/pkg/apis/v2beta2/receiver_webhook.go index 24d74632..ffec5e96 100644 --- a/pkg/apis/v2beta2/receiver_webhook.go +++ b/pkg/apis/v2beta2/receiver_webhook.go @@ -149,6 +149,21 @@ func (r *Receiver) validateReceiver() error { }) } + if r.Spec.Discord != nil && r.Spec.Discord.Webhook != nil { + credentials = append(credentials, map[string]interface{}{ + "credential": r.Spec.Discord.Webhook, + "path": field.NewPath("spec", "discord", "webhook"), + }) + if r.Spec.Discord.Type != nil { + if *r.Spec.Discord.Type != "content" && *r.Spec.Discord.Type != "embed" { + allErrs = append(allErrs, + field.NotSupported(field.NewPath("spec", "discord", "type"), + *r.Spec.Email.TmplType, + []string{"content", "embed"})) + } + } + } + for _, v := range credentials { err := validateCredential(v["credential"].(*Credential), v["path"].(*field.Path)) if err != nil { @@ -258,6 +273,20 @@ func (r *Receiver) validateReceiver() error { } } + if r.Spec.Discord != nil { + if r.Spec.Discord.Webhook == nil { + allErrs = append(allErrs, field.Required(field.NewPath("spec", "discord", "webhook"), + "must be specified")) + } + + if err := validateSelector(r.Spec.Discord.AlertSelector); err != nil { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "discord", "alertSelector"), + r.Spec.Discord.AlertSelector, + err.Error())) + } + } + if r.Spec.Pushover != nil { // validate User Profile if len(r.Spec.Pushover.Profiles) == 0 { diff --git a/pkg/apis/v2beta2/zz_generated.deepcopy.go b/pkg/apis/v2beta2/zz_generated.deepcopy.go index e00b4716..5325f9af 100644 --- a/pkg/apis/v2beta2/zz_generated.deepcopy.go +++ b/pkg/apis/v2beta2/zz_generated.deepcopy.go @@ -508,6 +508,81 @@ func (in *DingTalkReceiver) DeepCopy() *DingTalkReceiver { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DiscordOptions) DeepCopyInto(out *DiscordOptions) { + *out = *in + if in.NotificationTimeout != nil { + in, out := &in.NotificationTimeout, &out.NotificationTimeout + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscordOptions. +func (in *DiscordOptions) DeepCopy() *DiscordOptions { + if in == nil { + return nil + } + out := new(DiscordOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DiscordReceiver) DeepCopyInto(out *DiscordReceiver) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(Credential) + (*in).DeepCopyInto(*out) + } + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(string) + **out = **in + } + if in.TmplText != nil { + in, out := &in.TmplText, &out.TmplText + *out = new(ConfigmapKeySelector) + **out = **in + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.MentionedUsers != nil { + in, out := &in.MentionedUsers, &out.MentionedUsers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MentionedRoles != nil { + in, out := &in.MentionedRoles, &out.MentionedRoles + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AlertSelector != nil { + in, out := &in.AlertSelector, &out.AlertSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscordReceiver. +func (in *DiscordReceiver) DeepCopy() *DiscordReceiver { + if in == nil { + return nil + } + out := new(DiscordReceiver) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EmailConfig) DeepCopyInto(out *EmailConfig) { *out = *in @@ -1125,6 +1200,11 @@ func (in *Options) DeepCopyInto(out *Options) { *out = new(FeishuOptions) (*in).DeepCopyInto(*out) } + if in.Discord != nil { + in, out := &in.Discord, &out.Discord + *out = new(DiscordOptions) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Options. @@ -1444,6 +1524,11 @@ func (in *ReceiverSpec) DeepCopyInto(out *ReceiverSpec) { *out = new(FeishuReceiver) (*in).DeepCopyInto(*out) } + if in.Discord != nil { + in, out := &in.Discord, &out.Discord + *out = new(DiscordReceiver) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReceiverSpec. diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index ec8f5ae0..6cefeace 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -20,6 +20,10 @@ const ( SMS = "sms" Webhook = "webhook" WeChat = "wechat" + Discord = "discord" + + DiscordContent = "content" + DiscordEmbed = "embed" Namespace = "namespace" diff --git a/pkg/controller/factories.go b/pkg/controller/factories.go index 42340be8..546019c8 100644 --- a/pkg/controller/factories.go +++ b/pkg/controller/factories.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "github.com/kubesphere/notification-manager/pkg/internal/discord" "github.com/kubesphere/notification-manager/pkg/apis/v2beta2" "github.com/kubesphere/notification-manager/pkg/constants" @@ -34,6 +35,7 @@ func init() { receiverFactories[constants.SMS] = sms.NewReceiver receiverFactories[constants.Webhook] = webhook.NewReceiver receiverFactories[constants.WeChat] = wechat.NewReceiver + receiverFactories[constants.Discord] = discord.NewReceiver configFactories = make(map[string]configFactory) configFactories[constants.DingTalk] = dingtalk.NewConfig @@ -44,6 +46,7 @@ func init() { configFactories[constants.SMS] = sms.NewConfig configFactories[constants.Webhook] = webhook.NewConfig configFactories[constants.WeChat] = wechat.NewConfig + configFactories[constants.Discord] = discord.NewConfig } func NewReceivers(tenantID string, obj *v2beta2.Receiver) map[string]internal.Receiver { diff --git a/pkg/internal/discord/types.go b/pkg/internal/discord/types.go new file mode 100644 index 00000000..c2659d5c --- /dev/null +++ b/pkg/internal/discord/types.go @@ -0,0 +1,88 @@ +package discord + +import ( + "fmt" + "github.com/kubesphere/notification-manager/pkg/apis/v2beta2" + "github.com/kubesphere/notification-manager/pkg/constants" + "github.com/kubesphere/notification-manager/pkg/internal" +) + +type Receiver struct { + *internal.Common + Webhook *v2beta2.Credential `json:"webhook"` + Type *string `json:"type,omitempty"` + MentionedUsers []string `json:"mentionedUsers,omitempty"` + MentionedRoles []string `json:"mentionedRoles,omitempty"` +} + +func NewReceiver(tenantID string, obj *v2beta2.Receiver) internal.Receiver { + if obj.Spec.Discord == nil { + return nil + } + discord := obj.Spec.Discord + r := &Receiver{ + Common: &internal.Common{ + Name: obj.Name, + TenantID: tenantID, + Type: constants.Discord, + Labels: obj.Labels, + Enable: discord.Enabled, + AlertSelector: discord.AlertSelector, + Template: internal.Template{ + TmplName: *discord.Template, + TmplText: discord.TmplText, + }, + }, + MentionedRoles: discord.MentionedRoles, + MentionedUsers: discord.MentionedUsers, + Type: discord.Type, + } + + if discord.Webhook != nil { + r.Webhook = discord.Webhook + } + + return r +} + +func (r *Receiver) SetConfig(c internal.Config) { + return +} + +func (r *Receiver) Validate() error { + if r.Type != nil { + if *r.Type != constants.DiscordContent && *r.Type != constants.DiscordEmbed { + return fmt.Errorf("discord receiver: type must be one of: `content` or `embed`") + } + } + return nil +} + +func (r *Receiver) Clone() internal.Receiver { + + out := &Receiver{ + Common: r.Common.Clone(), + Webhook: r.Webhook, + } + + out.Type = r.Type + out.MentionedUsers = append(out.MentionedUsers, r.MentionedUsers...) + out.MentionedRoles = append(out.MentionedRoles, r.MentionedRoles...) + return out +} + +type Config struct { + *internal.Common +} + +func NewConfig(obj *v2beta2.Config) internal.Config { + return nil +} + +func (c *Config) Validate() error { + return nil +} + +func (c *Config) Clone() internal.Config { + return nil +} diff --git a/pkg/notify/notifier/discord/discord.go b/pkg/notify/notifier/discord/discord.go new file mode 100644 index 00000000..be39f84d --- /dev/null +++ b/pkg/notify/notifier/discord/discord.go @@ -0,0 +1,200 @@ +package discord + +import ( + "bytes" + "context" + "fmt" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/kubesphere/notification-manager/pkg/async" + "github.com/kubesphere/notification-manager/pkg/constants" + "github.com/kubesphere/notification-manager/pkg/controller" + "github.com/kubesphere/notification-manager/pkg/internal" + "github.com/kubesphere/notification-manager/pkg/internal/discord" + "github.com/kubesphere/notification-manager/pkg/notify/notifier" + "github.com/kubesphere/notification-manager/pkg/template" + "github.com/kubesphere/notification-manager/pkg/utils" + "net/http" + "strings" + "time" +) + +const ( + DefaultSendTimeout = time.Second * 3 + DefaultTextTemplate = `{{ template "nm.default.text" . }}` + MaxContentLength = 2000 + EmbedLimit = 4096 +) + +type Notifier struct { + notifierCtl *controller.Controller + receiver *discord.Receiver + timeout time.Duration + logger log.Logger + tmpl *template.Template +} +type Message struct { + Content string `json:"content"` + Embeds []Embeds `json:"embeds,omitempty"` +} + +type Embeds struct { + Description string `json:"description,omitempty"` +} + +func NewDiscordNotifier(logger log.Logger, receiver internal.Receiver, notifierCtl *controller.Controller) (notifier.Notifier, error) { + + n := &Notifier{ + notifierCtl: notifierCtl, + logger: logger, + timeout: DefaultSendTimeout, + } + + opts := notifierCtl.ReceiverOpts + tmplType := constants.Text + tmplName := "" + if opts != nil && opts.Global != nil && !utils.StringIsNil(opts.Global.Template) { + tmplName = opts.Global.Template + } + + if opts != nil && opts.Discord != nil { + if !utils.StringIsNil(opts.Discord.Template) { + tmplName = opts.Discord.Template + } + + if opts.Discord.NotificationTimeout != nil { + n.timeout = time.Second * time.Duration(*opts.Discord.NotificationTimeout) + } + } + + n.receiver = receiver.(*discord.Receiver) + if utils.StringIsNil(n.receiver.TmplType) { + n.receiver.TmplType = tmplType + } + + if utils.StringIsNil(n.receiver.TmplName) { + if tmplName != "" { + n.receiver.TmplName = tmplName + } else { + n.receiver.TmplName = DefaultTextTemplate + } + } + + var err error + n.tmpl, err = notifierCtl.GetReceiverTmpl(n.receiver.TmplText) + if err != nil { + _ = level.Error(n.logger).Log("msg", "DiscordNotifier: create receiver template error", "error", err.Error()) + return nil, err + } + return n, nil +} + +func (n *Notifier) Notify(ctx context.Context, data *template.Data) error { + + mentionedUsers := n.receiver.MentionedUsers + for i := range mentionedUsers { + if mentionedUsers[i] == "everyone" { + mentionedUsers[i] = "@everyone" + continue + } + mentionedUsers[i] = fmt.Sprintf("<@%s>", mentionedUsers[i]) + } + + mentionedRoles := n.receiver.MentionedRoles + for i := range mentionedRoles { + mentionedRoles[i] = fmt.Sprintf("<@&%s>", mentionedRoles[i]) + } + + atUsers := strings.Join(mentionedUsers, "") + atRoles := strings.Join(mentionedRoles, "") + var length int + if n.receiver.Type == nil || *n.receiver.Type == constants.DiscordContent { + length = MaxContentLength - len(atUsers) - len(atRoles) + } else { + length = EmbedLimit - len(atUsers) - len(atRoles) + } + messages, _, err := n.tmpl.Split(data, length, n.receiver.TmplName, "", n.logger) + if err != nil { + _ = level.Error(n.logger).Log("msg", "DiscordNotifier: generate message error", "error", err.Error()) + return err + } + + group := async.NewGroup(ctx) + if n.receiver.Webhook != nil { + for index := range messages { + group.Add(func(stopCh chan interface{}) { + msg := fmt.Sprintf("%s\n%s%s", messages[index], atUsers, atRoles) + stopCh <- n.sendTo(ctx, msg) + }) + } + } + return group.Wait() +} + +func (n *Notifier) sendTo(ctx context.Context, content string) error { + + message := &Message{} + if n.receiver.Type == nil || *n.receiver.Type == constants.DiscordContent { + message.Content = content + } else { + message.Embeds = []Embeds{ + { + Description: content, + }, + } + } + + send := func() (bool, error) { + url, err := n.notifierCtl.GetCredential(n.receiver.Webhook) + if err != nil { + return false, err + } + var buf bytes.Buffer + err = utils.JsonEncode(&buf, message) + if err != nil { + return false, err + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) + if err != nil { + return false, err + } + request.Header.Set("Content-Type", "application/json; charset=utf-8") + client := http.Client{} + resp, err := client.Do(request) + if err != nil { + return true, err + } + code := resp.StatusCode + level.Debug(n.logger).Log("msg", "DiscordNotifier", "response code:", code) + + if code != http.StatusNoContent { + return false, fmt.Errorf("DiscordNotifier: send message error, code: %d", code) + } + return false, nil + } + + start := time.Now() + defer func() { + _ = level.Debug(n.logger).Log("msg", "DiscordNotifier: send message", "used", time.Since(start).String()) + }() + + retry := 0 + MaxRetry := 3 + for { + if retry >= MaxRetry { + return fmt.Errorf("DiscordNotifier: send message error, retry %d times", retry) + } + needRetry, err := send() + if err != nil { + _ = level.Error(n.logger).Log("msg", "DiscordNotifier: send notification error", "error", err.Error()) + } + if needRetry { + retry = retry + 1 + time.Sleep(time.Second) + _ = level.Info(n.logger).Log("msg", "DiscordNotifier: retry to send notification", "retry", retry) + continue + } + + return err + } +} diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index 965f0f8a..fb945be3 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -11,6 +11,7 @@ import ( "github.com/kubesphere/notification-manager/pkg/internal" "github.com/kubesphere/notification-manager/pkg/notify/notifier" "github.com/kubesphere/notification-manager/pkg/notify/notifier/dingtalk" + "github.com/kubesphere/notification-manager/pkg/notify/notifier/discord" "github.com/kubesphere/notification-manager/pkg/notify/notifier/email" "github.com/kubesphere/notification-manager/pkg/notify/notifier/feishu" "github.com/kubesphere/notification-manager/pkg/notify/notifier/pushover" @@ -38,6 +39,7 @@ func init() { Register(constants.SMS, sms.NewSmsNotifier) Register(constants.Pushover, pushover.NewPushoverNotifier) Register(constants.Feishu, feishu.NewFeishuNotifier) + Register(constants.Discord, discord.NewDiscordNotifier) } func Register(name string, factory Factory) {