Skip to content

Commit 4b5c134

Browse files
committed
Formatted messages for Slack, Webex, Teams and Discord
1 parent 6a11b81 commit 4b5c134

File tree

4 files changed

+262
-21
lines changed

4 files changed

+262
-21
lines changed

controllers/notification.go

Lines changed: 228 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package controllers
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
23+
"regexp"
2224
"strconv"
2325
"strings"
2426

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

131-
message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
133+
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
134+
135+
msgSlack, err := composeSlackMessage(message, passing)
136+
if err != nil {
137+
l.V(logs.LogInfo).Info("failed to format slack message: %v", err)
138+
return err
139+
}
132140

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

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

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

158-
message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
167+
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
168+
169+
formattedMessage, err := composeWebexMessage(message, passing, logger)
170+
if err != nil {
171+
logger.V(logs.LogInfo).Info("failed to format webex message: %v", err)
172+
return err
173+
}
159174

160175
webexClient := webexteams.NewClient()
161176
if webexClient == nil {
@@ -172,6 +187,10 @@ func sendWebexNotification(ctx context.Context, c client.Client, clusterNamespac
172187
webexMessage := &webexteams.MessageCreateRequest{
173188
RoomID: info.room,
174189
Markdown: message,
190+
Attachments: []webexteams.Attachment{{
191+
ContentType: webexContentType,
192+
Content: formattedMessage,
193+
}},
175194
}
176195

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

202-
message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
221+
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
222+
223+
// Format Message
224+
discordReply, err := composeDiscordMessage(message, passing)
225+
if err != nil {
226+
l.V(logs.LogInfo).Info("failed to format discord message: %v", err)
227+
return err
228+
}
203229

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

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

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

231-
message, _ := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
258+
message, passing := getNotificationMessage(clusterNamespace, clusterName, clusterType, conditions, logger)
259+
260+
// Format message using adaptive cards
261+
card, err := composeTeamsMessage(message, passing, logger)
262+
if err != nil {
263+
l.V(logs.LogInfo).Info("failed to format teams message: %v", err)
264+
return err
265+
}
266+
267+
// Create message and attach card
268+
teamsMessage := &adaptivecard.Message{Type: adaptivecard.TypeMessage}
269+
if err := teamsMessage.Attach(card); err != nil {
270+
l.V(logs.LogInfo).Info("failed to add teams card: %v", err)
271+
return err
272+
}
273+
274+
// Prepare message
275+
if err := teamsMessage.Prepare(); err != nil {
276+
l.V(logs.LogInfo).Info("failed to prepare teams message payload: %v", err)
277+
return err
278+
}
279+
232280
teamsClient := goteamsnotify.NewTeamsClient()
233281

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

240-
// Create adaptive card with the clusterName as the title of the message
241-
teamsMessage, err := adaptivecard.NewSimpleMessage(message, clusterName, true)
242-
if err != nil {
243-
l.V(logs.LogInfo).Info("failed to create Teams message: %v", err)
244-
return err
245-
}
246-
247-
// Send the meesage with the user provided webhook URL
288+
// Send the message with the user provided webhook URL
248289
if teamsClient.Send(info.webhookUrl, teamsMessage) != nil {
249290
l.V(logs.LogInfo).Info("failed to send Teams message: %v", err)
250291
return err
@@ -302,18 +343,18 @@ func getNotificationMessage(clusterNamespace, clusterName string, clusterType li
302343
conditions []libsveltosv1beta1.Condition, logger logr.Logger) (string, bool) {
303344

304345
passing := true
305-
message := fmt.Sprintf("cluster %s:%s/%s \n", clusterType, clusterNamespace, clusterName)
346+
message := fmt.Sprintf("Cluster %s:%s/%s \n", clusterType, clusterNamespace, clusterName)
306347
for i := range conditions {
307348
c := &conditions[i]
308349
if c.Status != corev1.ConditionTrue {
309350
passing = false
310-
message += fmt.Sprintf("liveness check %q failing \n", c.Type)
351+
message += fmt.Sprintf("Liveness check %q failing \n", c.Type)
311352
message += fmt.Sprintf("%s \n", c.Message)
312353
}
313354
}
314355

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

489530
return &telegramInfo{token: string(authToken), chatID: chatID}, nil
490531
}
532+
533+
func composeDiscordMessage(message string, passing bool) ([]*discordgo.MessageEmbed, error) {
534+
lines := strings.Split(message, "\n")
535+
if len(lines) == 0 {
536+
embed := []*discordgo.MessageEmbed{{
537+
Type: discordgo.EmbedTypeRich,
538+
Title: "no message",
539+
}}
540+
return embed, fmt.Errorf("empty message")
541+
}
542+
543+
description := "Failing some checks."
544+
color := discordRed
545+
if passing {
546+
description = "Passing!"
547+
color = discordGreen
548+
}
549+
550+
content := []*discordgo.MessageEmbedField{}
551+
title := lines[0]
552+
name := ""
553+
val := ""
554+
empty := true
555+
556+
fail_reg := regexp.MustCompile(failedTestRegexp)
557+
558+
for _, line := range lines[1:] {
559+
if fail_reg.MatchString(line) {
560+
if name != "" {
561+
content = append(content, &discordgo.MessageEmbedField{Name: name, Value: val})
562+
empty = true
563+
}
564+
name = line
565+
} else if empty {
566+
val = line
567+
empty = false
568+
} else {
569+
val += "\n" + line
570+
}
571+
}
572+
content = append(content, &discordgo.MessageEmbedField{Name: name, Value: val})
573+
574+
embed := []*discordgo.MessageEmbed{{
575+
Type: discordgo.EmbedTypeRich,
576+
Title: title,
577+
Description: description,
578+
Color: color,
579+
Fields: content,
580+
}}
581+
return embed, nil
582+
}
583+
584+
func composeTeamsMessage(message string, passing bool, logger logr.Logger) (adaptivecard.Card, error) {
585+
card := adaptivecard.NewCard()
586+
587+
lines := strings.Split(message, "\n")
588+
if len(lines) == 0 {
589+
return card, fmt.Errorf("empty message")
590+
}
591+
592+
titleBlock := adaptivecard.NewTextBlock(lines[0], true)
593+
titleBlock.Weight = adaptivecard.WeightBolder
594+
titleBlock.Size = adaptivecard.SizeLarge
595+
596+
// Adding title to card
597+
if err := card.AddElement(false, titleBlock); err != nil {
598+
logger.V(logs.LogDebug).Info("error adding card")
599+
}
600+
601+
if passing {
602+
titleBlock.Text = "Passing! \n"
603+
titleBlock.Color = adaptivecard.ColorGood
604+
} else {
605+
titleBlock.Text = "Failing some checks. \n"
606+
titleBlock.Color = adaptivecard.ColorAttention
607+
}
608+
609+
// Adding msg summary -- all tests passing or some failing
610+
if err := card.AddElement(false, titleBlock); err != nil {
611+
logger.V(logs.LogDebug).Info("error adding card")
612+
}
613+
614+
// Adding remaining msg to card
615+
fail_regex := regexp.MustCompile(failedTestRegexp)
616+
617+
for _, line := range lines[1:] {
618+
textblock := adaptivecard.NewTextBlock(line, true)
619+
620+
if fail_regex.MatchString(line) {
621+
textblock.Color = adaptivecard.ColorAttention
622+
textblock.Separator = true
623+
} else {
624+
textblock.Separator = false
625+
textblock.Color = adaptivecard.ColorDefault
626+
}
627+
628+
if err := card.AddElement(false, textblock); err != nil {
629+
logger.V(logs.LogDebug).Info("error adding card")
630+
}
631+
}
632+
return card, nil
633+
}
634+
635+
func composeWebexMessage(message string, passing bool, logger logr.Logger) (map[string]interface{}, error) {
636+
cardData := map[string]interface{}{}
637+
638+
// using addaptivecard from teams formatting
639+
card, err := composeTeamsMessage(message, passing, logger)
640+
if err != nil {
641+
return cardData, err
642+
}
643+
644+
// fixing version/schema for webex compatibility
645+
card.Version = webexAdaptiveCardVersion
646+
card.Schema = webexAdaptiveCardSchema
647+
648+
// convert adaptiveCard to map[string]interface{}
649+
jsonBytes, err := json.MarshalIndent(card, "", " ")
650+
if err != nil {
651+
logger.V(logs.LogDebug).Info("Error Marshaling Card")
652+
return cardData, err
653+
}
654+
655+
err = json.Unmarshal(jsonBytes, &cardData)
656+
if err != nil {
657+
logger.V(logs.LogDebug).Info("Error Unmarshaling Card")
658+
return cardData, err
659+
}
660+
661+
// Remove added field for teams adaptive card : not required for webex
662+
delete(cardData, webexCardFieldMSTeams)
663+
664+
return cardData, nil
665+
}
666+
667+
func composeSlackMessage(message string, passing bool) (slack.Attachment, error) {
668+
attachment := slack.Attachment{
669+
MarkdownIn: []string{"text"},
670+
}
671+
672+
lines := strings.Split(message, "\n")
673+
if len(lines) == 0 {
674+
return attachment, fmt.Errorf("empty message")
675+
}
676+
677+
// adding color to summarize msg
678+
color := slackRed
679+
if passing {
680+
color = slackGreen
681+
}
682+
attachment.Color = color
683+
684+
// adding title
685+
attachment.Title = lines[0]
686+
687+
// adding the remaining msg
688+
markdownText := strings.Builder{}
689+
fail_reg := regexp.MustCompile(failedTestRegexp)
690+
691+
for _, line := range lines[1:] {
692+
if fail_reg.MatchString(line) {
693+
markdownText.WriteString("*" + line + "*\n")
694+
} else {
695+
markdownText.WriteString(line + "\n")
696+
}
697+
}
698+
attachment.Text = markdownText.String()
699+
return attachment, nil
700+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2023. projectsveltos.io. All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
const (
20+
webexAdaptiveCardVersion = "1.3"
21+
webexAdaptiveCardSchema = "http://adaptivecards.io/schemas/adaptive-card.json"
22+
webexCardFieldMSTeams = "msteams"
23+
webexContentType = "application/vnd.microsoft.card.adaptive"
24+
slackRed = "#E01E5A"
25+
slackGreen = "#36a64f"
26+
discordRed = 15598624
27+
discordGreen = 8311585
28+
failedTestRegexp = `Liveness\s+check\s+["']([^\"']*)["']\s+failing\s`
29+
)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/onsi/gomega v1.36.2
1515
github.com/pkg/errors v0.9.1
1616
github.com/projectsveltos/addon-controller v0.44.0
17-
github.com/projectsveltos/libsveltos v0.44.1-0.20250105090719-c7e096a8998f
17+
github.com/projectsveltos/libsveltos v0.44.1-0.20250107142400-7d20cef712ef
1818
github.com/prometheus/client_golang v1.20.5
1919
github.com/slack-go/slack v0.15.0
2020
github.com/spf13/pflag v1.0.5

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
130130
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
131131
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
132132
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
133+
github.com/mocktools/go-smtp-mock/v2 v2.4.0 h1:u0ky0iyNW/LEMKAFRTsDivHyP8dHYxe/cV3FZC3rRjo=
134+
github.com/mocktools/go-smtp-mock/v2 v2.4.0/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M=
133135
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
134136
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
135137
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -153,8 +155,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
153155
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
154156
github.com/projectsveltos/addon-controller v0.44.0 h1:WMEWHlPulFGKdq96P1sUu1E+3XXLuSNNVW3SA+yr20U=
155157
github.com/projectsveltos/addon-controller v0.44.0/go.mod h1:4GjQZyxvgNvD0joYN0l0eZDPauo0m299ZG4X9zilNJs=
156-
github.com/projectsveltos/libsveltos v0.44.1-0.20250105090719-c7e096a8998f h1:SVqsw6s+PNQfOtOnZefvNXeaRl5VRWNF8FGle07+Mjw=
157-
github.com/projectsveltos/libsveltos v0.44.1-0.20250105090719-c7e096a8998f/go.mod h1:ygOskqy32UUcH9P0Ygpei3oaNcyrcWWSQ+e4OxRX6QA=
158+
github.com/projectsveltos/libsveltos v0.44.1-0.20250107142400-7d20cef712ef h1:/3Av07+BV1vbnBscXO4YeDqo572F3BoKIROFS5wjEIU=
159+
github.com/projectsveltos/libsveltos v0.44.1-0.20250107142400-7d20cef712ef/go.mod h1:ygOskqy32UUcH9P0Ygpei3oaNcyrcWWSQ+e4OxRX6QA=
158160
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
159161
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
160162
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=

0 commit comments

Comments
 (0)