Skip to content

Commit

Permalink
Add MS Teams support
Browse files Browse the repository at this point in the history
Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>
  • Loading branch information
PrasadG193 committed Feb 2, 2020
1 parent 20f035a commit 3b39ad6
Show file tree
Hide file tree
Showing 13 changed files with 367 additions and 42 deletions.
43 changes: 25 additions & 18 deletions cmd/botkube/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,53 +19,60 @@ const (

func main() {
log.Logger.Info("Starting controller")
Config, err := config.New()
conf, err := config.New()
if err != nil {
log.Logger.Fatal(fmt.Sprintf("Error in loading configuration. Error:%s", err.Error()))
}

if Config.Communications.Slack.Enabled {
// List notifiers
var notifiers []notify.Notifier
if conf.Communications.Slack.Enabled {
log.Logger.Info("Starting slack bot")
sb := bot.NewSlackBot()
sb := bot.NewSlackBot(conf)
go sb.Start()
}

if Config.Communications.Mattermost.Enabled {
if conf.Communications.Mattermost.Enabled {
log.Logger.Info("Starting mattermost bot")
mb := bot.NewMattermostBot()
mb := bot.NewMattermostBot(conf)
go mb.Start()
}

if conf.Communications.Teams.Enabled {
log.Logger.Info("Starting MS Teams bot")
tb := bot.NewTeamsBot(conf)
notifiers = append(notifiers, tb)
go tb.Start()
}

// Prometheus metrics
metricsPort, exists := os.LookupEnv("METRICS_PORT")
if !exists {
metricsPort = defaultMetricsPort
}
go metrics.ServeMetrics(metricsPort)

// List notifiers
var notifiers []notify.Notifier
if Config.Communications.Slack.Enabled {
notifiers = append(notifiers, notify.NewSlack(Config))
if conf.Communications.Slack.Enabled {
notifiers = append(notifiers, notify.NewSlack(conf))
}
if Config.Communications.Mattermost.Enabled {
if notifier, err := notify.NewMattermost(Config); err == nil {
if conf.Communications.Mattermost.Enabled {
if notifier, err := notify.NewMattermost(conf); err == nil {
notifiers = append(notifiers, notifier)
}
}
if Config.Communications.ElasticSearch.Enabled {
notifiers = append(notifiers, notify.NewElasticSearch(Config))
if conf.Communications.ElasticSearch.Enabled {
notifiers = append(notifiers, notify.NewElasticSearch(conf))
}
if Config.Communications.Webhook.Enabled {
notifiers = append(notifiers, notify.NewWebhook(Config))
if conf.Communications.Webhook.Enabled {
notifiers = append(notifiers, notify.NewWebhook(conf))
}
if Config.Settings.UpgradeNotifier {
if conf.Settings.UpgradeNotifier {
log.Logger.Info("Starting upgrade notifier")
go controller.UpgradeNotifier(Config, notifiers)
go controller.UpgradeNotifier(conf, notifiers)
}

// Init KubeClient, InformerMap and start controller
utils.InitKubeClient()
utils.InitInformerMap()
controller.RegisterInformers(Config, notifiers)
controller.RegisterInformers(conf, notifiers)
}
7 changes: 7 additions & 0 deletions comm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ communications:
team: 'MATTERMOST_TEAM' # Mattermost Team to configure with BotKube
channel: 'MATTERMOST_CHANNEL' # Mattermost Channel for receiving BotKube alerts
notiftype: short # Change notification type short/long you want to receive. notiftype is optional and Default notification type is short (if not specified)

# Settings for MS Teams
teams:
enabled: false
appID: 'TEAMS_APP_ID'
appPassword: 'TEAMS_APP_PASSWORD'
notiftype: short

# Settings for ELS
elasticsearch:
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/gorilla/websocket v1.4.1 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/infracloudio/msbotbuilder-go v0.1.1-0.20200128183632-9d11322f671e
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/mattermost/gorp v2.0.0+incompatible // indirect
Expand All @@ -30,6 +31,7 @@ require (
github.com/onsi/ginkgo v1.10.2 // indirect
github.com/pborman/uuid v1.2.0 // indirect
github.com/pelletier/go-toml v1.5.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.2.1
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
Expand All @@ -44,11 +46,11 @@ require (
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v2 v2.2.8
k8s.io/api v0.17.0
k8s.io/apimachinery v0.17.0
k8s.io/client-go v0.17.0
k8s.io/kubectl v0.17.0
)

go 1.13
go 1.12
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0 h1:w3NnFcKR5241cfmQU5ZZAsf0xcpId6mWOupTvJlUX2U=
Expand Down Expand Up @@ -168,6 +169,9 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/infracloudio/msbotbuilder-go v0.1.0 h1:52h4uhEev67TTv4n4HNlo9rxr1/LiHShwYTtNBu5r5U=
github.com/infracloudio/msbotbuilder-go v0.1.1-0.20200128183632-9d11322f671e h1:rr89i/xWiD/l+MtGXF+LRz2K/wQ/SV1FabVPrEmHr3s=
github.com/infracloudio/msbotbuilder-go v0.1.1-0.20200128183632-9d11322f671e/go.mod h1:zTFZH9V4x9YQMXrBw2CNsI6hO6blIQ8jHNvdnjbAqZM=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
Expand All @@ -194,6 +198,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/lestrrat-go/jwx v0.9.0 h1:Fnd0EWzTm0kFrBPzE/PEPp9nzllES5buMkksPMjEKpM=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
Expand Down Expand Up @@ -253,6 +259,9 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -422,6 +431,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
9 changes: 2 additions & 7 deletions pkg/bot/mattermost.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,7 @@ type mattermostMessage struct {
}

// NewMattermostBot returns new Bot object
func NewMattermostBot() Bot {
c, err := config.New()
if err != nil {
logging.Logger.Fatalf("Error in loading configuration. Error: %s", err.Error())
}

func NewMattermostBot(c *config.Config) Bot {
return &MMBot{
ServerURL: c.Communications.Mattermost.URL,
Token: c.Communications.Mattermost.Token,
Expand Down Expand Up @@ -133,7 +128,7 @@ func (mm *mattermostMessage) handleMessage(b MMBot) {
// Trim the @BotKube prefix if exists
mm.Request = strings.TrimPrefix(post.Message, "@"+BotName+" ")

e := execute.NewDefaultExecutor(mm.Request, b.AllowKubectl, b.RestrictAccess, b.ClusterName, b.ChannelName, mm.IsAuthChannel)
e := execute.NewDefaultExecutor(mm.Request, b.AllowKubectl, b.RestrictAccess, b.ClusterName, mm.IsAuthChannel)
mm.Response = e.Execute()
mm.sendMessage()
}
Expand Down
9 changes: 2 additions & 7 deletions pkg/bot/slack.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package bot

import (
"fmt"
"strings"

"github.com/infracloudio/botkube/pkg/config"
Expand Down Expand Up @@ -33,11 +32,7 @@ type slackMessage struct {
}

// NewSlackBot returns new Bot object
func NewSlackBot() Bot {
c, err := config.New()
if err != nil {
logging.Logger.Fatal(fmt.Sprintf("Error in loading configuration. Error:%s", err.Error()))
}
func NewSlackBot(c *config.Config) Bot {
return &SlackBot{
Token: c.Communications.Slack.Token,
AllowKubectl: c.Settings.AllowKubectl,
Expand Down Expand Up @@ -139,7 +134,7 @@ func (sm *slackMessage) HandleMessage(b *SlackBot) {
return
}

e := execute.NewDefaultExecutor(sm.Request, b.AllowKubectl, b.RestrictAccess, b.ClusterName, b.ChannelName, sm.IsAuthChannel)
e := execute.NewDefaultExecutor(sm.Request, b.AllowKubectl, b.RestrictAccess, b.ClusterName, sm.IsAuthChannel)
sm.Response = e.Execute()
sm.Send()
}
Expand Down
148 changes: 148 additions & 0 deletions pkg/bot/teams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package bot

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/infracloudio/botkube/pkg/config"
"github.com/infracloudio/botkube/pkg/events"
"github.com/infracloudio/botkube/pkg/execute"
"github.com/infracloudio/botkube/pkg/logging"
"github.com/infracloudio/msbotbuilder-go/core"
coreActivity "github.com/infracloudio/msbotbuilder-go/core/activity"
"github.com/infracloudio/msbotbuilder-go/schema"
)

const (
defaultMsgPath = "/api/messages"
defaultPort = "3978"
)

// Teams contains credentials to start Teams backend server
type Teams struct {
AppID string
AppPassword string
MessagePath string
Port string
AllowKubectl bool
RestrictAccess bool
ClusterName string
NotifType config.NotifType
Adapter core.Adapter

ConversationRef schema.ConversationReference
}

// NewTeamsBot returns Teams instance
func NewTeamsBot(c *config.Config) *Teams {
logging.Logger.Infof("Config:: %+v", c.Communications.Teams)
return &Teams{
AppID: c.Communications.Teams.AppID,
AppPassword: c.Communications.Teams.AppPassword,
NotifType: c.Communications.Teams.NotifType,
MessagePath: defaultMsgPath,
Port: defaultPort,
AllowKubectl: c.Settings.AllowKubectl,
RestrictAccess: c.Settings.RestrictAccess,
ClusterName: c.Settings.ClusterName,
}
}

// Start MS Teams server to serve messages from Teams client
func (t *Teams) Start() {
var err error
setting := core.AdapterSetting{
AppID: t.AppID,
AppPassword: t.AppPassword,
}
t.Adapter, err = core.NewBotAdapter(setting)
if err != nil {
logging.Logger.Errorf("Failed Start teams bot. %+v", err)
return
}
http.HandleFunc(t.MessagePath, t.processActivity)
logging.Logger.Infof("Started MS Teams server on port %s", defaultPort)
logging.Logger.Errorf("Error in MS Teams server. %v", http.ListenAndServe(fmt.Sprintf(":%s", t.Port), nil))
}

func (t *Teams) processActivity(w http.ResponseWriter, req *http.Request) {
ctx := context.Background()
activity, err := t.Adapter.ParseRequest(ctx, req)
if err != nil {
logging.Logger.Errorf("Failed to parse Teams request. %s", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

err = t.Adapter.ProcessActivity(ctx, activity, coreActivity.HandlerFuncs{
OnMessageFunc: func(turn *coreActivity.TurnContext) (schema.Activity, error) {
actjson, _ := json.MarshalIndent(turn.Activity, "", " ")
logging.Logger.Debugf("Received activity: %s", actjson)
return turn.SendActivity(coreActivity.MsgOptionText(t.processMessage(turn.Activity)))
},
})
if err != nil {
logging.Logger.Errorf("Failed to process request. %s", err.Error())
}
}

func (t *Teams) processMessage(activity schema.Activity) string {
// Trim @BotKube prefix
msg := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(activity.Text), "<at>BotKube</at>"))

// Parse "set default channel" command and set conversation reference
if msg == "set default channel" {
t.ConversationRef = coreActivity.GetCoversationReference(activity)
// Remove messageID from the ChannelID
if ID, ok := activity.ChannelData["teamsChannelId"]; ok {
t.ConversationRef.ChannelID = ID.(string)
t.ConversationRef.Conversation.ID = ID.(string)
}
return "Okay. I'll send notifications to this channel"
}

// Multicluster is not supported for Teams
e := execute.NewDefaultExecutor(msg, t.AllowKubectl, t.RestrictAccess, t.ClusterName, true)
return fmt.Sprintf("```%s\n%s```", t.ClusterName, e.Execute())
}

func (t *Teams) SendEvent(event events.Event) error {
card := formatTeamsMessage(event, t.NotifType)
if err := t.sendProactiveMessage(card); err != nil {
logging.Logger.Errorf("Failed to send notification. %s", err.Error())
}
logging.Logger.Debugf("Event successfully sent to MS Teams >> %+v", event)
return nil
}

// SendMessage sends message to MsTeams
func (t *Teams) SendMessage(msg string) error {
err := t.Adapter.ProactiveMessage(context.TODO(), t.ConversationRef, coreActivity.HandlerFuncs{
OnMessageFunc: func(turn *coreActivity.TurnContext) (schema.Activity, error) {
return turn.SendActivity(coreActivity.MsgOptionText(msg))
},
})
if err != nil {
return err
}
logging.Logger.Debug("Message successfully sent to MS Teams")
return nil
}

func (t *Teams) sendProactiveMessage(card map[string]interface{}) error {
err := t.Adapter.ProactiveMessage(context.TODO(), t.ConversationRef, coreActivity.HandlerFuncs{
OnMessageFunc: func(turn *coreActivity.TurnContext) (schema.Activity, error) {
attachments := []schema.Attachment{
{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: card,
},
}
return turn.SendActivity(coreActivity.MsgOptionAttachments(attachments))
},
})
return err
}
Loading

0 comments on commit 3b39ad6

Please sign in to comment.