diff --git a/README.md b/README.md index 74748f8..bd20624 100644 --- a/README.md +++ b/README.md @@ -254,3 +254,23 @@ docker run -d \ -e WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=watchtower-server-1 \ v2tec/watchtower ``` + +### Notifications via MSTeams incoming webhook + +To receive notifications in MSTeams channel, add `msteams` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable. + +Additionally, you should set the MSTeams webhook url using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable. + +MSTeams notifier could send keys/values filled by ```log.WithField``` or ```log.WithFields``` as MSTeams message facts. To enable this feature add `--notification-msteams-data` flag or set `WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true` environment variable. + +Example: + +```bash +docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATIONS=msteams \ + -e WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL="https://outlook.office.com/webhook/xxxxxxxx@xxxxxxx/IncomingWebhook/yyyyyyyy/zzzzzzzzzz" \ + -e WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true \ + v2tec/watchtower +``` diff --git a/main.go b/main.go index 97fe15d..2b6fba5 100644 --- a/main.go +++ b/main.go @@ -101,7 +101,7 @@ func main() { cli.StringSliceFlag{ Name: "notifications", Value: &cli.StringSlice{}, - Usage: "notification types to send (valid: email, slack)", + Usage: "notification types to send (valid: email, slack, msteams)", EnvVar: "WATCHTOWER_NOTIFICATIONS", }, cli.StringFlag{ @@ -161,6 +161,16 @@ func main() { EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", Value: "watchtower", }, + cli.StringFlag{ + Name: "notification-msteams-hook", + Usage: "The MSTeams WebHook URL to send notifications to", + EnvVar: "WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL", + }, + cli.BoolFlag{ + Name: "notification-msteams-data", + Usage: "The MSTeams notifier will try to extract log entry fields as MSTeams message facts", + EnvVar: "WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA", + }, } if err := app.Run(os.Args); err != nil { diff --git a/notifications/msteams.go b/notifications/msteams.go new file mode 100644 index 0000000..8bb9d7a --- /dev/null +++ b/notifications/msteams.go @@ -0,0 +1,134 @@ +package notifications + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + log "github.com/sirupsen/logrus" + "github.com/urfave/cli" + "io/ioutil" +) + +const ( + msTeamsType = "msteams" +) + +type msTeamsTypeNotifier struct { + webHookURL string + levels []log.Level + data bool +} + +func newMsTeamsNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifier { + + webHookURL := c.GlobalString("notification-msteams-hook") + if len(webHookURL) <= 0 { + log.Fatal("Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.") + } + + n := &msTeamsTypeNotifier{ + levels: acceptedLogLevels, + webHookURL: webHookURL, + data: c.GlobalBool("notification-msteams-data"), + } + + log.AddHook(n) + + return n +} + +func (n *msTeamsTypeNotifier) StartNotification() {} + +func (n *msTeamsTypeNotifier) SendNotification() {} + +func (n *msTeamsTypeNotifier) Levels() []log.Level { + return n.levels +} + +func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { + + message := "(" + entry.Level.String() + "): " + entry.Message + + go func() { + webHookBody := messageCard{ + CardType: "MessageCard", + Context: "http://schema.org/extensions", + Markdown: true, + Text: message, + } + + if n.data && entry.Data != nil && len(entry.Data) > 0 { + section := messageCardSection{ + Facts: make([]messageCardSectionFact, len(entry.Data)), + Text: "", + } + + index := 0 + for k, v := range entry.Data { + section.Facts[index] = messageCardSectionFact{ + Name: k, + Value: fmt.Sprint(v), + } + index++ + } + + webHookBody.Sections = []messageCardSection{section} + } + + jsonBody, err := json.Marshal(webHookBody) + if err != nil { + fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err) + return + } + + resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) + if err != nil { + fmt.Println("Failed to send MSTeams notificattion: ", err) + } + + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + fmt.Println("Failed to send MSTeams notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode) + if resp.Body != nil { + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err == nil { + bodyString := string(bodyBytes) + fmt.Println(bodyString) + } + } + } + }() + + return nil +} + +type messageCard struct { + CardType string `json:"@type"` + Context string `json:"@context"` + CorrelationID string `json:"correlationId,omitempty"` + ThemeColor string `json:"themeColor,omitempty"` + Summary string `json:"summary,omitempty"` + Title string `json:"title,omitempty"` + Text string `json:"text,omitempty"` + Markdown bool `json:"markdown,bool"` + Sections []messageCardSection `json:"sections,omitempty"` +} + +type messageCardSection struct { + Title string `json:"title,omitempty"` + Text string `json:"text,omitempty"` + ActivityTitle string `json:"activityTitle,omitempty"` + ActivitySubtitle string `json:"activitySubtitle,omitempty"` + ActivityImage string `json:"activityImage,omitempty"` + ActivityText string `json:"activityText,omitempty"` + HeroImage string `json:"heroImage,omitempty"` + Facts []messageCardSectionFact `json:"facts,omitempty"` +} + +type messageCardSectionFact struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} diff --git a/notifications/notifier.go b/notifications/notifier.go index 274811b..62e8ebc 100644 --- a/notifications/notifier.go +++ b/notifications/notifier.go @@ -36,6 +36,8 @@ func NewNotifier(c *cli.Context) *Notifier { tn = newEmailNotifier(c, acceptedLogLevels) case slackType: tn = newSlackNotifier(c, acceptedLogLevels) + case msTeamsType: + tn = newMsTeamsNotifier(c, acceptedLogLevels) default: log.Fatalf("Unknown notification type %q", t) }