Skip to content

Commit 6a7bc49

Browse files
committed
Added MS Teams webhook support for drift notifications.
Signed-off-by: Lorenzo Buitizon <[email protected]>
1 parent 136dc4a commit 6a7bc49

File tree

5 files changed

+267
-4
lines changed

5 files changed

+267
-4
lines changed

backend/controllers/orgs.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ func GetOrgSettingsApi(c *gin.Context) {
3939
}
4040

4141
c.JSON(http.StatusOK, gin.H{
42-
"drift_enabled": org.DriftEnabled,
43-
"drift_cron_tab": org.DriftCronTab,
44-
"drift_webhook_url": org.DriftWebhookUrl,
45-
"billing_plan": org.BillingPlan,
42+
"drift_enabled": org.DriftEnabled,
43+
"drift_cron_tab": org.DriftCronTab,
44+
"drift_webhook_url": org.DriftWebhookUrl,
45+
"drift_teams_webhook_url": org.DriftTeamsWebhookUrl,
46+
"billing_plan": org.BillingPlan,
4647
})
4748
}
4849

@@ -66,6 +67,7 @@ func UpdateOrgSettingsApi(c *gin.Context) {
6667
DriftEnabled *bool `json:"drift_enabled,omitempty"`
6768
DriftCronTab *string `json:"drift_cron_tab,omitempty"`
6869
DriftWebhookUrl *string `json:"drift_webhook_url,omitempty"`
70+
DriftTeamsWebhookUrl *string `json:"drift_teams_webhook_url,omitempty"`
6971
BillingPlan *string `json:"billing_plan,omitempty"`
7072
BillingStripeSubscriptionId *string `json:"billing_stripe_subscription_id,omitempty"`
7173
SlackChannelName *string `json:"slack_channel_name,omitempty"`
@@ -89,6 +91,10 @@ func UpdateOrgSettingsApi(c *gin.Context) {
8991
org.DriftWebhookUrl = *reqBody.DriftWebhookUrl
9092
}
9193

94+
if reqBody.DriftTeamsWebhookUrl != nil {
95+
org.DriftTeamsWebhookUrl = *reqBody.DriftTeamsWebhookUrl
96+
}
97+
9298
if reqBody.BillingPlan != nil {
9399
org.BillingPlan = models.BillingPlan(*reqBody.BillingPlan)
94100
}

backend/models/orgs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Organisation struct {
2323
ExternalId string `gorm:"uniqueIndex:idx_external_source"`
2424
DriftEnabled bool `gorm:"default:false"`
2525
DriftWebhookUrl string
26+
DriftTeamsWebhookUrl string
2627
DriftCronTab string `gorm:"default:'0 0 * * *'"`
2728
BillingPlan BillingPlan `gorm:"default:'free'"`
2829
BillingStripeSubscriptionId string

ee/drift/controllers/notifications.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,73 @@ func sendTestSlackWebhook(webhookURL string) error {
7878
return nil
7979
}
8080

81+
func sendTestTeamsWebhook(webhookURL string) error {
82+
messageCard := map[string]interface{}{
83+
"@type": "MessageCard",
84+
"@context": "http://schema.org/extensions",
85+
"themeColor": "0076D7",
86+
"summary": "Digger Drift Detection Test",
87+
"sections": []map[string]interface{}{
88+
{
89+
"activityTitle": "Digger Drift Detection",
90+
"activitySubtitle": "Test Notification",
91+
"activityText": "This is a test notification to verify your MS Teams integration is working correctly.",
92+
"facts": []map[string]string{
93+
{"name": "Project", "value": "Dev environment"},
94+
{"name": "Status", "value": "🟡 Drift detected"},
95+
},
96+
},
97+
{
98+
"activityTitle": "Environment Status",
99+
"activitySubtitle": "Current Status Overview",
100+
"facts": []map[string]string{
101+
{"name": "Dev environment", "value": "🟡 Drift detected"},
102+
{"name": "Staging environment", "value": "⚪ Acknowledged drift"},
103+
{"name": "Prod environment", "value": "🟢 No drift"},
104+
},
105+
},
106+
{
107+
"activityTitle": "Note",
108+
"activityText": "✅ This is a test notification",
109+
},
110+
},
111+
"potentialAction": []map[string]interface{}{
112+
{
113+
"@type": "OpenUri",
114+
"name": "View Dashboard",
115+
"targets": []map[string]interface{}{
116+
{"os": "default", "uri": os.Getenv("DIGGER_APP_URL")},
117+
},
118+
},
119+
},
120+
}
121+
122+
jsonPayload, err := json.Marshal(messageCard)
123+
if err != nil {
124+
return fmt.Errorf("error marshalling JSON: %v", err)
125+
}
126+
127+
resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(jsonPayload))
128+
if err != nil {
129+
return fmt.Errorf("error sending POST request: %v", err)
130+
}
131+
defer resp.Body.Close()
132+
133+
if resp.StatusCode != http.StatusOK {
134+
return fmt.Errorf("non-OK HTTP status: %s", resp.Status)
135+
}
136+
137+
return nil
138+
}
139+
81140
type TestSlackNotificationForUrl struct {
82141
SlackNotificationUrl string `json:"notification_url"`
83142
}
84143

144+
type TestTeamsNotificationForUrl struct {
145+
TeamsNotificationUrl string `json:"notification_url"`
146+
}
147+
85148
func (mc MainController) SendTestSlackNotificationForUrl(c *gin.Context) {
86149
var request TestSlackNotificationForUrl
87150
err := c.BindJSON(&request)
@@ -102,6 +165,26 @@ func (mc MainController) SendTestSlackNotificationForUrl(c *gin.Context) {
102165
c.String(200, "ok")
103166
}
104167

168+
func (mc MainController) SendTestTeamsNotificationForUrl(c *gin.Context) {
169+
var request TestTeamsNotificationForUrl
170+
err := c.BindJSON(&request)
171+
if err != nil {
172+
log.Printf("Error binding JSON: %v", err)
173+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error binding JSON"})
174+
return
175+
}
176+
teamsNotificationUrl := request.TeamsNotificationUrl
177+
178+
err = sendTestTeamsWebhook(teamsNotificationUrl)
179+
if err != nil {
180+
log.Printf("Error sending teams notification: %v", err)
181+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error sending teams notification"})
182+
return
183+
}
184+
185+
c.String(200, "ok")
186+
}
187+
105188
func sectionBlockForProject(project models.Project) (*slack.SectionBlock, error) {
106189
switch project.DriftStatus {
107190
case models.DriftStatusNoDrift:
@@ -217,6 +300,128 @@ func (mc MainController) SendRealSlackNotificationForOrg(c *gin.Context) {
217300
c.String(200, "ok")
218301
}
219302

303+
func createTeamsMessageCardForProjects(projects []*models.Project) map[string]interface{} {
304+
facts := []map[string]string{
305+
{"name": "Project", "value": "Status"},
306+
}
307+
308+
var sections []map[string]interface{}
309+
310+
for _, project := range projects {
311+
if project.DriftEnabled {
312+
var statusValue string
313+
314+
switch project.DriftStatus {
315+
case models.DriftStatusNoDrift:
316+
statusValue = "🟢 No Drift"
317+
case models.DriftStatusAcknowledgeDrift:
318+
statusValue = "⚪ Acknowledged Drift"
319+
case models.DriftStatusNewDrift:
320+
statusValue = "🟡 Drift detected"
321+
default:
322+
statusValue = "❓ Unknown"
323+
}
324+
325+
facts = append(facts, map[string]string{
326+
"name": project.Name,
327+
"value": statusValue,
328+
})
329+
}
330+
}
331+
332+
section := map[string]interface{}{
333+
"activityTitle": "Digger Drift Detection Report",
334+
"activitySubtitle": fmt.Sprintf("Found %d projects with drift enabled", len(facts)-1),
335+
"facts": facts,
336+
}
337+
338+
sections = append(sections, section)
339+
340+
messageCard := map[string]interface{}{
341+
"@type": "MessageCard",
342+
"@context": "http://schema.org/extensions",
343+
"themeColor": "0076D7",
344+
"summary": "Digger Drift Detection Report",
345+
"sections": sections,
346+
"potentialAction": []map[string]interface{}{
347+
{
348+
"@type": "OpenUri",
349+
"name": "View Dashboard",
350+
"targets": []map[string]interface{}{
351+
{"os": "default", "uri": os.Getenv("DIGGER_APP_URL")},
352+
},
353+
},
354+
},
355+
}
356+
357+
return messageCard
358+
}
359+
360+
func (mc MainController) SendRealTeamsNotificationForOrg(c *gin.Context) {
361+
var request RealSlackNotificationForOrgRequest
362+
err := c.BindJSON(&request)
363+
if err != nil {
364+
log.Printf("Error binding JSON: %v", err)
365+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error binding JSON"})
366+
return
367+
}
368+
orgId := request.OrgId
369+
370+
org, err := models.DB.GetOrganisationById(orgId)
371+
if err != nil {
372+
log.Printf("could not get org %v err: %v", orgId, err)
373+
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Could not get org %v", orgId)})
374+
return
375+
}
376+
377+
teamsNotificationUrl := org.DriftTeamsWebhookUrl
378+
379+
projects, err := models.DB.LoadProjectsForOrg(orgId)
380+
if err != nil {
381+
log.Printf("could not load projects for org %v err: %v", orgId, err)
382+
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Could not load projects for org %v", orgId)})
383+
return
384+
}
385+
386+
numOfProjectsWithDriftEnabled := 0
387+
for _, project := range projects {
388+
if project.DriftEnabled {
389+
numOfProjectsWithDriftEnabled++
390+
}
391+
}
392+
393+
if numOfProjectsWithDriftEnabled == 0 {
394+
log.Printf("no projects with drift enabled for org: %v, succeeding", orgId)
395+
c.String(200, "ok")
396+
return
397+
}
398+
399+
messageCard := createTeamsMessageCardForProjects(projects)
400+
401+
jsonPayload, err := json.Marshal(messageCard)
402+
if err != nil {
403+
log.Printf("error marshalling teams message card: %v", err)
404+
c.JSON(500, gin.H{"error": "error marshalling teams message card"})
405+
return
406+
}
407+
408+
resp, err := http.Post(teamsNotificationUrl, "application/json", bytes.NewBuffer(jsonPayload))
409+
if err != nil {
410+
log.Printf("error sending teams webhook: %v", err)
411+
c.JSON(500, gin.H{"error": "error sending teams webhook"})
412+
return
413+
}
414+
defer resp.Body.Close()
415+
416+
if resp.StatusCode != http.StatusOK {
417+
log.Printf("teams webhook got unexpected status for org: %v - status: %v", org.ID, resp.StatusCode)
418+
c.JSON(500, gin.H{"error": "teams webhook got unexpected status"})
419+
return
420+
}
421+
422+
c.String(200, "ok")
423+
}
424+
220425
func (mc MainController) ProcessAllNotifications(c *gin.Context) {
221426
diggerHostname := os.Getenv("DIGGER_HOSTNAME")
222427
webhookSecret := os.Getenv("DIGGER_WEBHOOK_SECRET")
@@ -233,6 +438,13 @@ func (mc MainController) ProcessAllNotifications(c *gin.Context) {
233438
return
234439
}
235440

441+
sendTeamsNotificationUrl, err := url.JoinPath(diggerHostname, "_internal/send_teams_notification_for_org")
442+
if err != nil {
443+
log.Printf("could not form teams drift url: %v", err)
444+
c.JSON(500, gin.H{"error": "could not form teams drift url"})
445+
return
446+
}
447+
236448
for _, org := range orgs {
237449
if org.DriftEnabled == false {
238450
continue
@@ -280,6 +492,45 @@ func (mc MainController) ProcessAllNotifications(c *gin.Context) {
280492
if statusCode != 200 {
281493
log.Printf("send slack notification got unexpected status for org: %v - status: %v", org.ID, statusCode)
282494
}
495+
496+
// Send MS Teams notification if webhook URL is configured
497+
if org.DriftTeamsWebhookUrl != "" {
498+
fmt.Println("Sending teams notification for org ID: ", org.ID)
499+
teamsPayload := RealSlackNotificationForOrgRequest{OrgId: org.ID}
500+
501+
// Convert payload to JSON
502+
teamsJsonPayload, err := json.Marshal(teamsPayload)
503+
if err != nil {
504+
fmt.Println("Process Teams notification: error marshaling JSON:", err)
505+
continue
506+
}
507+
508+
// Create a new request for MS Teams
509+
teamsReq, err := http.NewRequest("POST", sendTeamsNotificationUrl, bytes.NewBuffer(teamsJsonPayload))
510+
if err != nil {
511+
fmt.Println("Process teams notification: Error creating request:", err)
512+
continue
513+
}
514+
515+
// Set headers
516+
teamsReq.Header.Set("Content-Type", "application/json")
517+
teamsReq.Header.Set("Authorization", fmt.Sprintf("Bearer %v", webhookSecret))
518+
519+
// Send the request
520+
teamsClient := &http.Client{}
521+
teamsResp, err := teamsClient.Do(teamsReq)
522+
if err != nil {
523+
fmt.Println("Error sending teams request:", err)
524+
continue
525+
}
526+
teamsResp.Body.Close()
527+
528+
// Get the status code
529+
teamsStatusCode := teamsResp.StatusCode
530+
if teamsStatusCode != 200 {
531+
log.Printf("send teams notification got unexpected status for org: %v - status: %v", org.ID, teamsStatusCode)
532+
}
533+
}
283534
}
284535
}
285536

ee/drift/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ func main() {
9292
r.POST("/_internal/process_notifications", middleware.WebhookAuth(), controller.ProcessAllNotifications)
9393
r.POST("/_internal/send_slack_notification_for_org", middleware.WebhookAuth(), controller.SendRealSlackNotificationForOrg)
9494
r.POST("/_internal/send_test_slack_notification_for_url", middleware.WebhookAuth(), controller.SendTestSlackNotificationForUrl)
95+
r.POST("/_internal/send_teams_notification_for_org", middleware.WebhookAuth(), controller.SendRealTeamsNotificationForOrg)
96+
r.POST("/_internal/send_test_teams_notification_for_url", middleware.WebhookAuth(), controller.SendTestTeamsNotificationForUrl)
9597

9698
r.POST("/_internal/process_drift", middleware.WebhookAuth(), controller.ProcessAllDrift)
9799
r.POST("/_internal/process_drift_for_org", middleware.WebhookAuth(), controller.ProcessDriftForOrg)

go.work.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,7 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
947947
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
948948
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
949949
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
950+
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
950951
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
951952
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE=
952953
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
@@ -1100,6 +1101,7 @@ github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMc
11001101
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
11011102
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
11021103
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
1104+
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11031105
github.com/googleapis/cloud-bigtable-clients-test v0.0.2 h1:S+sCHWAiAc+urcEnvg5JYJUOdlQEm/SEzQ/c/IdAH5M=
11041106
github.com/googleapis/cloud-bigtable-clients-test v0.0.2/go.mod h1:mk3CrkrouRgtnhID6UZQDK3DrFFa7cYCAJcEmNsHYrY=
11051107
github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
@@ -1462,6 +1464,7 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B
14621464
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
14631465
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
14641466
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
1467+
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
14651468
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig=
14661469
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
14671470
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=

0 commit comments

Comments
 (0)