Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formatted messages for Slack, Webex, Teams and Discord #267

Merged
merged 1 commit into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 228 additions & 18 deletions controllers/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package controllers

import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"

Expand Down Expand Up @@ -128,7 +130,13 @@ func sendSlackNotification(ctx context.Context, c client.Client, clusterNamespac
l := logger.WithValues("channel", info.channelID)
l.V(logs.LogInfo).Info("send slack message")

message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)

msgSlack, err := composeSlackMessage(message, passing)
if err != nil {
l.V(logs.LogInfo).Info("failed to format slack message: %v", err)
return err
}

api := slack.New(info.token)
if api == nil {
Expand All @@ -137,7 +145,8 @@ func sendSlackNotification(ctx context.Context, c client.Client, clusterNamespac

l.V(logs.LogDebug).Info(fmt.Sprintf("Sending message to channel %s", info.channelID))

_, _, err = api.PostMessage(info.channelID, slack.MsgOptionText(message, false))
_, _, err = api.PostMessage(info.channelID, slack.MsgOptionText("ProjectSveltos Updates", false),
slack.MsgOptionAttachments(msgSlack))
if err != nil {
l.V(logs.LogInfo).Info(fmt.Sprintf("Failed to send message. Error: %v", err))
return err
Expand All @@ -155,7 +164,13 @@ func sendWebexNotification(ctx context.Context, c client.Client, clusterNamespac
return err
}

message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)

formattedMessage, err := composeWebexMessage(message, passing, logger)
if err != nil {
logger.V(logs.LogInfo).Info("failed to format webex message: %v", err)
return err
}

webexClient := webexteams.NewClient()
if webexClient == nil {
Expand All @@ -172,6 +187,10 @@ func sendWebexNotification(ctx context.Context, c client.Client, clusterNamespac
webexMessage := &webexteams.MessageCreateRequest{
RoomID: info.room,
Markdown: message,
Attachments: []webexteams.Attachment{{
ContentType: webexContentType,
Content: formattedMessage,
}},
}

_, resp, err := webexClient.Messages.CreateMessage(webexMessage)
Expand Down Expand Up @@ -199,7 +218,14 @@ func sendDiscordNotification(ctx context.Context, c client.Client, clusterNamesp
l := logger.WithValues("channel", info.channelID)
l.V(logs.LogInfo).Info("send discord message")

message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)

// Format Message
discordReply, err := composeDiscordMessage(message, passing)
if err != nil {
l.V(logs.LogInfo).Info("failed to format discord message: %v", err)
return err
}

// Create a new Discord session using the provided token
dg, err := discordgo.New("Bot " + info.token)
Expand All @@ -208,9 +234,10 @@ func sendDiscordNotification(ctx context.Context, c client.Client, clusterNamesp
return err
}

// Create a new message with both a text content and the file attachment
// Send message with formatted message in embeds
_, err = dg.ChannelMessageSendComplex(info.channelID, &discordgo.MessageSend{
Content: message,
Content: "ProjectSveltos Updates",
Embeds: discordReply,
})

return err
Expand All @@ -228,7 +255,28 @@ func sendTeamsNotification(ctx context.Context, c client.Client, clusterNamespac
l := logger.WithValues("webhookUrl", info.webhookUrl)
l.V(logs.LogInfo).Info("send teams message")

message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)

// Format message using adaptive cards
card, err := composeTeamsMessage(message, passing, logger)
if err != nil {
l.V(logs.LogInfo).Info("failed to format teams message: %v", err)
return err
}

// Create message and attach card
teamsMessage := &adaptivecard.Message{Type: adaptivecard.TypeMessage}
if err := teamsMessage.Attach(card); err != nil {
l.V(logs.LogInfo).Info("failed to add teams card: %v", err)
return err
}

// Prepare message
if err := teamsMessage.Prepare(); err != nil {
l.V(logs.LogInfo).Info("failed to prepare teams message payload: %v", err)
return err
}

teamsClient := goteamsnotify.NewTeamsClient()

// Validate Teams Webhook expected format
Expand All @@ -237,14 +285,7 @@ func sendTeamsNotification(ctx context.Context, c client.Client, clusterNamespac
return err
}

// Create adaptive card with the clusterName as the title of the message
teamsMessage, err := adaptivecard.NewSimpleMessage(message, clusterName, true)
if err != nil {
l.V(logs.LogInfo).Info("failed to create Teams message: %v", err)
return err
}

// Send the meesage with the user provided webhook URL
// Send the message with the user provided webhook URL
if teamsClient.Send(info.webhookUrl, teamsMessage) != nil {
l.V(logs.LogInfo).Info("failed to send Teams message: %v", err)
return err
Expand Down Expand Up @@ -302,18 +343,18 @@ func getNotificationMessage(clusterNamespace, clusterName string, clusterType li
conditions []libsveltosv1beta1.Condition, logger logr.Logger) (string, bool) {

passing := true
message := fmt.Sprintf("cluster %s:%s/%s \n", clusterType, clusterNamespace, clusterName)
message := fmt.Sprintf("Cluster %s:%s/%s \n", clusterType, clusterNamespace, clusterName)
for i := range conditions {
c := &conditions[i]
if c.Status != corev1.ConditionTrue {
passing = false
message += fmt.Sprintf("liveness check %q failing \n", c.Type)
message += fmt.Sprintf("Liveness check %q failing \n", c.Type)
message += fmt.Sprintf("%s \n", c.Message)
}
}

if passing {
message += "all liveness checks are passing"
message += "All liveness checks are passing"
logger.V(logs.LogDebug).Info("all liveness checks are passing")
} else {
logger.V(logs.LogDebug).Info("some of the liveness checks are not passing")
Expand Down Expand Up @@ -488,3 +529,172 @@ func getTelegramInfo(ctx context.Context, c client.Client, n *libsveltosv1beta1.

return &telegramInfo{token: string(authToken), chatID: chatID}, nil
}

func composeDiscordMessage(message string, passing bool) ([]*discordgo.MessageEmbed, error) {
lines := strings.Split(message, "\n")
if len(lines) == 0 {
embed := []*discordgo.MessageEmbed{{
Type: discordgo.EmbedTypeRich,
Title: "no message",
}}
return embed, fmt.Errorf("empty message")
}

description := "Failing some checks."
color := discordRed
if passing {
description = "Passing!"
color = discordGreen
}

content := []*discordgo.MessageEmbedField{}
title := lines[0]
name := ""
val := ""
empty := true

fail_reg := regexp.MustCompile(failedTestRegexp)

for _, line := range lines[1:] {
if fail_reg.MatchString(line) {
if name != "" {
content = append(content, &discordgo.MessageEmbedField{Name: name, Value: val})
empty = true
}
name = line
} else if empty {
val = line
empty = false
} else {
val += "\n" + line
}
}
content = append(content, &discordgo.MessageEmbedField{Name: name, Value: val})

embed := []*discordgo.MessageEmbed{{
Type: discordgo.EmbedTypeRich,
Title: title,
Description: description,
Color: color,
Fields: content,
}}
return embed, nil
}

func composeTeamsMessage(message string, passing bool, logger logr.Logger) (adaptivecard.Card, error) {
card := adaptivecard.NewCard()

lines := strings.Split(message, "\n")
if len(lines) == 0 {
return card, fmt.Errorf("empty message")
}

titleBlock := adaptivecard.NewTextBlock(lines[0], true)
titleBlock.Weight = adaptivecard.WeightBolder
titleBlock.Size = adaptivecard.SizeLarge

// Adding title to card
if err := card.AddElement(false, titleBlock); err != nil {
logger.V(logs.LogDebug).Info("error adding card")
}

if passing {
titleBlock.Text = "Passing! \n"
titleBlock.Color = adaptivecard.ColorGood
} else {
titleBlock.Text = "Failing some checks. \n"
titleBlock.Color = adaptivecard.ColorAttention
}

// Adding msg summary -- all tests passing or some failing
if err := card.AddElement(false, titleBlock); err != nil {
logger.V(logs.LogDebug).Info("error adding card")
}

// Adding remaining msg to card
fail_regex := regexp.MustCompile(failedTestRegexp)

for _, line := range lines[1:] {
textblock := adaptivecard.NewTextBlock(line, true)

if fail_regex.MatchString(line) {
textblock.Color = adaptivecard.ColorAttention
textblock.Separator = true
} else {
textblock.Separator = false
textblock.Color = adaptivecard.ColorDefault
}

if err := card.AddElement(false, textblock); err != nil {
logger.V(logs.LogDebug).Info("error adding card")
}
}
return card, nil
}

func composeWebexMessage(message string, passing bool, logger logr.Logger) (map[string]interface{}, error) {
cardData := map[string]interface{}{}

// using addaptivecard from teams formatting
card, err := composeTeamsMessage(message, passing, logger)
if err != nil {
return cardData, err
}

// fixing version/schema for webex compatibility
card.Version = webexAdaptiveCardVersion
card.Schema = webexAdaptiveCardSchema

// convert adaptiveCard to map[string]interface{}
jsonBytes, err := json.MarshalIndent(card, "", " ")
if err != nil {
logger.V(logs.LogDebug).Info("Error Marshaling Card")
return cardData, err
}

err = json.Unmarshal(jsonBytes, &cardData)
if err != nil {
logger.V(logs.LogDebug).Info("Error Unmarshaling Card")
return cardData, err
}

// Remove added field for teams adaptive card : not required for webex
delete(cardData, webexCardFieldMSTeams)

return cardData, nil
}

func composeSlackMessage(message string, passing bool) (slack.Attachment, error) {
attachment := slack.Attachment{
MarkdownIn: []string{"text"},
}

lines := strings.Split(message, "\n")
if len(lines) == 0 {
return attachment, fmt.Errorf("empty message")
}

// adding color to summarize msg
color := slackRed
if passing {
color = slackGreen
}
attachment.Color = color

// adding title
attachment.Title = lines[0]

// adding the remaining msg
markdownText := strings.Builder{}
fail_reg := regexp.MustCompile(failedTestRegexp)

for _, line := range lines[1:] {
if fail_reg.MatchString(line) {
markdownText.WriteString("*" + line + "*\n")
} else {
markdownText.WriteString(line + "\n")
}
}
attachment.Text = markdownText.String()
return attachment, nil
}
29 changes: 29 additions & 0 deletions controllers/notification_constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2023. projectsveltos.io. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

const (
webexAdaptiveCardVersion = "1.3"
webexAdaptiveCardSchema = "http://adaptivecards.io/schemas/adaptive-card.json"
webexCardFieldMSTeams = "msteams"
webexContentType = "application/vnd.microsoft.card.adaptive"
slackRed = "#E01E5A"
slackGreen = "#36a64f"
discordRed = 15598624
discordGreen = 8311585
failedTestRegexp = `Liveness\s+check\s+["']([^\"']*)["']\s+failing\s`
)
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/onsi/gomega v1.36.2
github.com/pkg/errors v0.9.1
github.com/projectsveltos/addon-controller v0.44.0
github.com/projectsveltos/libsveltos v0.44.1-0.20250105090719-c7e096a8998f
github.com/projectsveltos/libsveltos v0.44.1-0.20250107142400-7d20cef712ef
github.com/prometheus/client_golang v1.20.5
github.com/slack-go/slack v0.15.0
github.com/spf13/pflag v1.0.5
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mocktools/go-smtp-mock/v2 v2.4.0 h1:u0ky0iyNW/LEMKAFRTsDivHyP8dHYxe/cV3FZC3rRjo=
github.com/mocktools/go-smtp-mock/v2 v2.4.0/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -153,8 +155,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/projectsveltos/addon-controller v0.44.0 h1:WMEWHlPulFGKdq96P1sUu1E+3XXLuSNNVW3SA+yr20U=
github.com/projectsveltos/addon-controller v0.44.0/go.mod h1:4GjQZyxvgNvD0joYN0l0eZDPauo0m299ZG4X9zilNJs=
github.com/projectsveltos/libsveltos v0.44.1-0.20250105090719-c7e096a8998f h1:SVqsw6s+PNQfOtOnZefvNXeaRl5VRWNF8FGle07+Mjw=
github.com/projectsveltos/libsveltos v0.44.1-0.20250105090719-c7e096a8998f/go.mod h1:ygOskqy32UUcH9P0Ygpei3oaNcyrcWWSQ+e4OxRX6QA=
github.com/projectsveltos/libsveltos v0.44.1-0.20250107142400-7d20cef712ef h1:/3Av07+BV1vbnBscXO4YeDqo572F3BoKIROFS5wjEIU=
github.com/projectsveltos/libsveltos v0.44.1-0.20250107142400-7d20cef712ef/go.mod h1:ygOskqy32UUcH9P0Ygpei3oaNcyrcWWSQ+e4OxRX6QA=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
Expand Down
Loading