Skip to content

Commit

Permalink
add teams to chatops
Browse files Browse the repository at this point in the history
  • Loading branch information
tmu-sprd authored and dhollinger committed Jun 5, 2024
1 parent 37176b4 commit d1c5d93
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Default: false
#### `service`

Type: string
Description: Which service to use. Supported options: [`slack`, `rocketchat`]
Description: Which service to use. Supported options: [`slack`, `rocketchat`, `teams`]
Default: nil

#### `channel`
Expand Down
4 changes: 2 additions & 2 deletions api/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ func (e EnvironmentController) DeployEnvironment(c *gin.Context) {
c.JSON(http.StatusInternalServerError, res)
c.Abort()
if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusInternalServerError, env)
conn.PostMessage(http.StatusInternalServerError, env, res)
}
return
}

c.JSON(http.StatusAccepted, res)
if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusAccepted, env)
conn.PostMessage(http.StatusAccepted, env, res)
}

}
10 changes: 7 additions & 3 deletions api/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (m ModuleController) DeployModule(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error Parsing Webhook", "error": err})
c.Abort()
if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusInternalServerError, "Error Parsing Webhook")
conn.PostMessage(http.StatusInternalServerError, "Error Parsing Webhook", err)
}
return
}
Expand All @@ -63,6 +63,10 @@ func (m ModuleController) DeployModule(c *gin.Context) {
if !match {
c.JSON(http.StatusInternalServerError, gin.H{"message": "Invalid module name"})
c.Abort()
err = fmt.Errorf("invalid module name: module name does not match the expected pattern; got: %s, pattern: ^[a-z][a-z0-9_]*$", overrideModule)
if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusInternalServerError, "Invalid module name", err)
}
return
}
module = overrideModule
Expand Down Expand Up @@ -103,13 +107,13 @@ func (m ModuleController) DeployModule(c *gin.Context) {
c.JSON(http.StatusInternalServerError, res)
c.Abort()
if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusInternalServerError, data.ModuleName)
conn.PostMessage(http.StatusInternalServerError, data.ModuleName, err)
}
return
}

c.JSON(http.StatusAccepted, res)
if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusAccepted, data.ModuleName)
conn.PostMessage(http.StatusAccepted, data.ModuleName, res)
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.19

require (
code.gitea.io/gitea/modules/structs v0.0.0-20190610152049-835b53fc259c
github.com/atc0005/go-teams-notify/v2 v2.10.0
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/webhooks/v6 v6.3.0
github.com/google/go-github/v39 v39.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.gitea.io/gitea/modules/structs v0.0.0-20190610152049-835b53fc259c h1:WwxK+8qmKYgU2pfcbCeRSqKwEPeHnW/sfmNc6pjLZC8=
code.gitea.io/gitea/modules/structs v0.0.0-20190610152049-835b53fc259c/go.mod h1:e/Ukqo229PbsSEymXfLWmNz4g04hwnFml5lW6U+0Azs=
github.com/atc0005/go-teams-notify/v2 v2.10.0 h1:eQvRIkyESQgBvlUdQ/iPol/lj3QcRyrdEQM3+c/nXhM=
github.com/atc0005/go-teams-notify/v2 v2.10.0/go.mod h1:SIeE1UfCcVRYMqP5b+r1ZteHyA/2UAjzWF5COnZ8q0w=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
Expand Down
2 changes: 2 additions & 0 deletions lib/chatops/chatops-interfaces.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package chatops

import (
"github.com/atc0005/go-teams-notify/v2/adaptivecard"
"github.com/pandatix/gocket-chat/api/chat"
)

type ChatOpsInterface interface {
PostMessage(code int, target string) (*ChatOpsResponse, error)
slack(code int, target string) (*string, *string, error)
rocketChat(code int, target string) (*chat.PostMessageResponse, error)
teams(code int, target string, output interface{}) (*adaptivecard.Message, error)
}
7 changes: 6 additions & 1 deletion lib/chatops/chatops.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type ChatAttachment struct {
Color string
}

func (c *ChatOps) PostMessage(code int, target string) (*ChatOpsResponse, error) {
func (c *ChatOps) PostMessage(code int, target string, output interface{}) (*ChatOpsResponse, error) {
var resp ChatOpsResponse

switch c.Service {
Expand All @@ -45,6 +45,11 @@ func (c *ChatOps) PostMessage(code int, target string) (*ChatOpsResponse, error)
}
resp.Channel = res.Channel
resp.Timestamp = strconv.FormatInt(res.Ts, 10)
case "teams":
_, err := c.teams(code, target, output)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("ChatOps tools `%s` is not supported at this time", c.Service)
}
Expand Down
35 changes: 32 additions & 3 deletions lib/chatops/chatops_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package chatops

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -21,7 +22,7 @@ func Test_PostMessage(t *testing.T) {
TestURL: &mockServer.Server.URL,
}

resp, err := c.PostMessage(202, "main")
resp, err := c.PostMessage(202, "main", "output")

assert.NoError(t, err, "should not error")
assert.Equal(t, resp.Channel, c.Channel, "channel should be correct")
Expand All @@ -31,7 +32,7 @@ func Test_PostMessage(t *testing.T) {
assert.Equal(t, mockServer.Received.Attachment[0].Color, "green")
assert.Equal(t, mockServer.Received.Attachment[0].Text, "Successfully started deployment of main")

resp, err = c.PostMessage(500, "main")
resp, err = c.PostMessage(500, "main", "output")

assert.NoError(t, err, "should not error")

Expand All @@ -48,10 +49,38 @@ func Test_PostMessage(t *testing.T) {
TestMode: true,
}

_, err := c.PostMessage(202, "main")
_, err := c.PostMessage(202, "main", "output")

assert.Error(t, err, "A ServerURI must be specified to use RocketChat")

})
t.Run("Teams", func(t *testing.T) {
serverURI := "https://example.webhook.office.com/webhook/xxx"
c := ChatOps{
Service: "teams",
TestMode: true,
ServerURI: &serverURI,
}

_, err := c.PostMessage(202, "main", "output")

assert.NoError(t, err, "should not error")

_, err = c.PostMessage(500, "main", "output")

assert.NoError(t, err, "should not error")

_, err = c.PostMessage(500, "main", fmt.Errorf("error"))

assert.NoError(t, err, "should not error")

serverURI = "https://doesnotexist.at"
c.TestMode = false
_, err = c.PostMessage(202, "main", "output")
assert.Error(t, err, "should error")
errorMessage := err.Error()
assert.Contains(t, errorMessage, "failed to validate webhook URL", "The error message should contain the specific substring")

})
})
}
174 changes: 174 additions & 0 deletions lib/chatops/teams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package chatops

import (
"fmt"
"runtime"
"strings"

goteamsnotify "github.com/atc0005/go-teams-notify/v2"
"github.com/atc0005/go-teams-notify/v2/adaptivecard"
log "github.com/sirupsen/logrus"
)

// Sends a message to a webhook in Microsoft Teams. Returns AdaptiveCard message and error.
func (c *ChatOps) teams(code int, target string, output interface{}) (*adaptivecard.Message, error) {
var color string
var status string
var messageTextBlock adaptivecard.Element
var detailsBlock adaptivecard.Element
var details bool

// Initialize a new Microsoft Teams client.
mstClient := goteamsnotify.NewTeamsClient()

// Set webhook url.
webhookUrl := *c.ServerURI

// Get caller function name (either module or environment).
callerFunction := getCaller()

// The title for message (first TextBlock element).
msgTitle := "Deploy " + callerFunction + " " + target

// Formatted message body.
msgText := "r10k output:\n\n" + fmt.Sprintf("%s", output)

// Create blank card.
card := adaptivecard.NewCard()

// Determine text color and status text
if code == 202 {
if ScanforWarn(output) {
color = adaptivecard.ColorWarning
status = "successful with Warnings"
} else {
color = adaptivecard.ColorGood
status = "successful"
}
} else {
color = adaptivecard.ColorAttention
status = "failed"
}

// Create title element.
headerTextBlock := NewTitleTextBlock(msgTitle, color)

// Change texts and add details button, depending on type of output (error, QueueItem, any)
switch fmt.Sprintf("%T", output) {
case "error":
messageTextBlock = NewTextBlock(fmt.Sprintf("Error: %s", output), color)
details = false
case "*queue.QueueItem":
messageTextBlock = NewTextBlock(fmt.Sprintf("%s %s added to queue", callerFunction, target), color)
details = false
default:
messageTextBlock = NewTextBlock(fmt.Sprintf("Deployment of %s %s", target, status), color)
details = true
detailsBlock = adaptivecard.NewHiddenTextBlock(msgText, true)
detailsBlock.ID = "detailsBlock"
}

// This grouping is used for convenience.
allTextBlocks := []adaptivecard.Element{
headerTextBlock,
messageTextBlock,
}

// Add "Details" button to hide/unhide detailed output.
if details {
allTextBlocks = append(allTextBlocks, detailsBlock)
toggleButton := adaptivecard.NewActionToggleVisibility("Details")
if err := toggleButton.AddTargetElement(nil, detailsBlock); err != nil {
log.Errorf(
"failed to add element ID to toggle button: %v",
err,
)
return nil, err
}

if err := card.AddAction(true, toggleButton); err != nil {
log.Errorf(
"failed to add toggle button action to card: %v",
err,
)
return nil, err
}
}

// Assemble card from all elements.
if err := card.AddElement(true, allTextBlocks...); err != nil {
log.Errorf(
"failed to add text blocks to card: %v",
err,
)
return nil, err
}

// Create new Message using Card as input.
msg, err := adaptivecard.NewMessageFromCard(card)
if err != nil {
log.Errorf("failed to create message from card: %v", err)
return nil, err
}

// If not testing, sende the message.
if !c.TestMode {
if err := mstClient.Send(webhookUrl, msg); err != nil {
log.Errorf("failed to send message: %v", err)
return nil, err
}
}

return msg, err
}

// Scans output for string "WARN". Returns true, if found.
func ScanforWarn(output interface{}) bool {
asstring := fmt.Sprintf("%s", output)
return strings.Contains(asstring, "WARN")
}

// Gets caller function. Returns "environment", "module" or empty string.
func getCaller() string {
var callerFunction string

pc, _, _, ok := runtime.Caller(3)
runtimedetails := runtime.FuncForPC(pc)
if ok && runtimedetails != nil {
splitStr := strings.Split(runtimedetails.Name(), ".")
switch splitStr[len(splitStr)-1] {
case "DeployEnvironment":
callerFunction = "environment"
case "DeployModule":
callerFunction = "module"
default:
callerFunction = ""
}
}
return callerFunction
}

// Creates title text block. Returns AdaptiveCard element.
func NewTitleTextBlock(title string, color string) adaptivecard.Element {
return adaptivecard.Element{
Type: adaptivecard.TypeElementTextBlock,
Wrap: true,
Text: title,
Style: adaptivecard.TextBlockStyleHeading,
Size: adaptivecard.SizeLarge,
Weight: adaptivecard.WeightBolder,
Color: color,
}
}

// Creates text block. Returns AdaptiveCard element.
func NewTextBlock(text string, color string) adaptivecard.Element {
textBlock := adaptivecard.Element{
Type: adaptivecard.TypeElementTextBlock,
Wrap: true,
Text: text,
Color: color,
}

return textBlock
}
3 changes: 2 additions & 1 deletion lib/queue/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,13 @@ func worker() {
log.Errorf("failed to execute local command `%s` with error: `%s` `%s`", job.Command, err, res)

if conf.ChatOps.Enabled {
conn.PostMessage(http.StatusInternalServerError, job.Name)
conn.PostMessage(http.StatusInternalServerError, job.Name, res)
}
job.State = "failed"
continue
}

conn.PostMessage(http.StatusAccepted, job.Name, res)
job.State = "success"
}
}

0 comments on commit d1c5d93

Please sign in to comment.