From 782119ef47b5deef1f64c629347709cb829cbde3 Mon Sep 17 00:00:00 2001 From: yufeng Date: Thu, 22 May 2025 11:03:49 +0800 Subject: [PATCH] Implement Feishu sink for Kubernetes event notifications This commit introduces a new Feishu sink that allows sending Kubernetes event notifications to Feishu (Lark) via custom bot webhooks. The README has been updated to include configuration examples for the Feishu receiver. The new FeishuSink struct is implemented to handle message formatting and sending, including support for customizable message fields and card colors. Additionally, the receiver configuration has been updated to support the new Feishu sink. Co-authored-by: [reckless] --- README.md | 22 +++++ pkg/sinks/feishu.go | 208 ++++++++++++++++++++++++++++++++++++++++++ pkg/sinks/receiver.go | 5 + 3 files changed, 235 insertions(+) create mode 100644 pkg/sinks/feishu.go diff --git a/README.md b/README.md index f74bd5f4..751b34e8 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,28 @@ receivers: layout: # Optional ``` +### Feishu (Lark) + +[Feishu](https://www.feishu.cn/) (Lark internationally) is an all-in-one collaboration platform. kubernetes-event-exporter can send events to Feishu via custom bot webhooks, allowing you to receive Kubernetes events in your Feishu groups. Below is an example configuration: + +```yaml +# ... +receivers: + - name: "feishu-alerts" + feishu: + webhookUrl: "https://open.feishu.cn/open-apis/bot/v2/hook/your-webhook-token" + message: "Kubernetes Event Notification: {{ .Message }}" + title: "Kubernetes Event - {{ .Reason }}" + color: "blue" # Card color: blue, wathet, turquoise, green, yellow, orange, red, carmine, violet, purple, indigo, grey + fields: + Message: "{{ .Message }}" + Namespace: "{{ .Namespace }}" + Reason: "{{ .Reason }}" + Object: "{{ .InvolvedObject.Kind }}/{{ .InvolvedObject.Name }}" + Type: "{{ .Type }}" + Count: "{{ .Count }}" +``` + ### Elasticsearch [Elasticsearch](https://www.elastic.co/) is a full-text, distributed search engine which can also do powerful diff --git a/pkg/sinks/feishu.go b/pkg/sinks/feishu.go new file mode 100644 index 00000000..3fae458a --- /dev/null +++ b/pkg/sinks/feishu.go @@ -0,0 +1,208 @@ +package sinks + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sort" + "time" + + "github.com/resmoio/kubernetes-event-exporter/pkg/kube" + "github.com/rs/zerolog/log" +) + +// FeishuConfig defines the configuration for Feishu webhook +type FeishuConfig struct { + WebhookURL string `yaml:"webhookUrl"` // Feishu bot webhook URL + Message string `yaml:"message"` // Message text + Title string `yaml:"title"` // Message card title + Fields map[string]string `yaml:"fields"` // Message fields + Color string `yaml:"color"` // Card color +} + +// FeishuSink implements the Feishu receiver +type FeishuSink struct { + cfg *FeishuConfig + client *http.Client +} + +// NewFeishuSink creates a new Feishu sink +func NewFeishuSink(cfg *FeishuConfig) (Sink, error) { + return &FeishuSink{ + cfg: cfg, + client: &http.Client{ + Timeout: time.Second * 30, + }, + }, nil +} + +// Feishu message card structures +type feishuMessage struct { + MsgType string `json:"msg_type"` + Card feishuMessageCard `json:"card"` +} + +type feishuMessageCard struct { + Header feishuCardHeader `json:"header"` + Elements []feishuElement `json:"elements"` + Config feishuConfig `json:"config,omitempty"` +} + +type feishuCardHeader struct { + Title feishuCardText `json:"title"` + Template string `json:"template,omitempty"` // Card color, options: blue, wathet, turquoise, green, yellow, orange, red, carmine, violet, purple, indigo, grey +} + +type feishuCardText struct { + Content string `json:"content"` + Tag string `json:"tag"` +} + +type feishuElement struct { + Tag string `json:"tag"` + Text feishuCardText `json:"text,omitempty"` + Fields []feishuField `json:"fields,omitempty"` + Elements []feishuField `json:"elements,omitempty"` +} + +type feishuField struct { + Text feishuCardText `json:"text,omitempty"` + IsShort bool `json:"is_short,omitempty"` +} + +type feishuConfig struct { + WideScreenMode bool `json:"wide_screen_mode"` +} + +// Send sends a message to Feishu +func (s *FeishuSink) Send(ctx context.Context, ev *kube.EnhancedEvent) error { + message, err := GetString(ev, s.cfg.Message) + if err != nil { + return err + } + + // Build Feishu message + msg := feishuMessage{ + MsgType: "interactive", + Card: feishuMessageCard{ + Config: feishuConfig{ + WideScreenMode: true, + }, + }, + } + + // Set title + title := message + if s.cfg.Title != "" { + title, err = GetString(ev, s.cfg.Title) + if err != nil { + return err + } + } + + // Set title and color + msg.Card.Header = feishuCardHeader{ + Title: feishuCardText{ + Content: title, + Tag: "plain_text", + }, + } + + // Set card color + if s.cfg.Color != "" { + colorValue, err := GetString(ev, s.cfg.Color) + if err != nil { + return err + } + msg.Card.Header.Template = colorValue + } else { + // Default blue + msg.Card.Header.Template = "blue" + } + + // Add message text element + msg.Card.Elements = append(msg.Card.Elements, feishuElement{ + Tag: "div", + Text: feishuCardText{ + Content: message, + Tag: "plain_text", + }, + }) + + // If fields are configured, add field content + if s.cfg.Fields != nil && len(s.cfg.Fields) > 0 { + fields := make([]feishuField, 0) + + // Sort fields to ensure consistent order + keys := make([]string, 0, len(s.cfg.Fields)) + for k := range s.cfg.Fields { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := s.cfg.Fields[k] + fieldText, err := GetString(ev, v) + if err != nil { + return err + } + + fields = append(fields, feishuField{ + Text: feishuCardText{ + Content: fmt.Sprintf("**%s**: %s", k, fieldText), + Tag: "lark_md", + }, + }) + } + + msg.Card.Elements = append(msg.Card.Elements, feishuElement{ + Tag: "div", + Elements: fields, + }) + } + + // Convert to JSON + jsonData, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("error marshaling feishu message: %w", err) + } + + // Send POST request + req, err := http.NewRequestWithContext(ctx, "POST", s.cfg.WebhookURL, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(req) + if err != nil { + return fmt.Errorf("error sending request to feishu: %w", err) + } + defer resp.Body.Close() + + // Read response + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + + // Check response status + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("feishu webhook returned non-200 status code: %d, body: %s", resp.StatusCode, string(body)) + } + + log.Debug(). + Str("status", resp.Status). + Str("body", string(body)). + Msg("Feishu webhook response") + + return nil +} + +// Close closes the sink +func (f *FeishuSink) Close() { + // No actual operation needed +} diff --git a/pkg/sinks/receiver.go b/pkg/sinks/receiver.go index 21fc35a5..f08b0b73 100644 --- a/pkg/sinks/receiver.go +++ b/pkg/sinks/receiver.go @@ -19,6 +19,7 @@ type ReceiverConfig struct { SQS *SQSConfig `yaml:"sqs"` SNS *SNSConfig `yaml:"sns"` Slack *SlackConfig `yaml:"slack"` + Feishu *FeishuConfig `yaml:"feishu"` Kafka *KafkaConfig `yaml:"kafka"` Pubsub *PubsubConfig `yaml:"pubsub"` Opscenter *OpsCenterConfig `yaml:"opscenter"` @@ -94,6 +95,10 @@ func (r *ReceiverConfig) GetSink() (Sink, error) { return NewSlackSink(r.Slack) } + if r.Feishu != nil { + return NewFeishuSink(r.Feishu) + } + if r.Kafka != nil { return NewKafkaSink(r.Kafka) }