diff --git a/README.md b/README.md index bb1d1cc..35f8936 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,9 @@ Watchtower can send notifications when containers are updated. Notifications are The types of notifications to send are passed via the comma-separated option `--notifications` (or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values: * `email` to send notifications via e-mail +* `discord` to send notifications into Discord channel +### Email To receive notifications by email, the following command-line options, or their corresponding environment variables, can be set: * `--notification-email-from` (env. `WATCHTOWER_NOTIFICATION_EMAIL_FROM`): The e-mail address from which notifications will be sent. @@ -214,3 +216,11 @@ docker run -d \ v2tec/watchtower ``` +### Discord +To send notifications into Discord channel, the following command-line options, or their corresponding environment variables, can be set: + +* `--notification-discord-webhook` (env. `WATCHTOWER_NOTIFICATION_DISCORD_WEBHOOK`): Webhook URL configured in Discord. + +[Checkout how to add webhook for channel](https://support.discordapp.com/hc/en-us/articles/228383668) + + diff --git a/main.go b/main.go index 1fd015b..f71f28d 100644 --- a/main.go +++ b/main.go @@ -91,42 +91,47 @@ func main() { Usage: "enable debug mode with verbose logging", }, cli.StringSliceFlag{ - Name: "notifications", - Value: &cli.StringSlice{}, - Usage: "notification types to send (valid: email)", + Name: "notifications", + Value: &cli.StringSlice{}, + Usage: "notification types to send (valid: email, discord)", EnvVar: "WATCHTOWER_NOTIFICATIONS", }, cli.StringFlag{ - Name: "notification-email-from", - Usage: "Address to send notification e-mails from", + Name: "notification-email-from", + Usage: "Address to send notification e-mails from", EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_FROM", }, cli.StringFlag{ - Name: "notification-email-to", - Usage: "Address to send notification e-mails to", + Name: "notification-email-to", + Usage: "Address to send notification e-mails to", EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_TO", }, cli.StringFlag{ - Name: "notification-email-server", - Usage: "SMTP server to send notification e-mails through", + Name: "notification-email-server", + Usage: "SMTP server to send notification e-mails through", EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER", }, cli.IntFlag{ - Name: "notification-email-server-port", - Usage: "SMTP server port to send notification e-mails through", + Name: "notification-email-server-port", + Usage: "SMTP server port to send notification e-mails through", Value: 25, EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", }, cli.StringFlag{ - Name: "notification-email-server-user", - Usage: "SMTP server user for sending notifications", + Name: "notification-email-server-user", + Usage: "SMTP server user for sending notifications", EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER", }, cli.StringFlag{ - Name: "notification-email-server-password", - Usage: "SMTP server password for sending notifications", + Name: "notification-email-server-password", + Usage: "SMTP server password for sending notifications", EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", }, + cli.StringFlag{ + Name: "notification-discord-webhook", + Usage: "Discord webhook url for sending notifications", + EnvVar: "WATCHTOWER_NOTIFICATION_DISCORD_WEBHOOK", + }, } if err := app.Run(os.Args); err != nil { @@ -180,7 +185,7 @@ func start(c *cli.Context) error { scheduleSpec, func() { select { - case v := <- tryLockSem: + case v := <-tryLockSem: defer func() { tryLockSem <- v }() notifier.StartNotification() if err := actions.Update(client, names, cleanup, noRestart); err != nil { diff --git a/notifications/discord.go b/notifications/discord.go new file mode 100644 index 0000000..f6ac3cb --- /dev/null +++ b/notifications/discord.go @@ -0,0 +1,96 @@ +package notifications + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + log "github.com/Sirupsen/logrus" + "github.com/urfave/cli" +) + +const ( + discordType = "discord" +) + +type discordTypeNotifier struct { + WebhookURL string + entries []*log.Entry +} + +func newDiscordNotifier(c *cli.Context) typeNotifier { + n := &discordTypeNotifier{ + WebhookURL: c.GlobalString("notification-discord-webhook"), + } + + log.AddHook(n) + + return n +} + +func (n *discordTypeNotifier) sendEntries(entries []*log.Entry) { + // Do the sending in a separate goroutine so we don't block the main process. + message := "" + for _, entry := range entries { + message += entry.Time.Format("2006-01-02 15:04:05") + " (" + entry.Level.String() + "): " + entry.Message + "\r\n" + } + + go func() { + webhookBody := discordWebhookBody{Content: message} + jsonBody, err := json.Marshal(webhookBody) + if err != nil { + fmt.Println("Failed to build JSON body for Discord notificattion: ", err) + return + } + + resp, err := http.Post(n.WebhookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) + if err != nil { + fmt.Println("Failed to send Discord notificattion: ", err) + } + + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + fmt.Println("Failed to send Discord notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode) + } + }() +} + +func (n *discordTypeNotifier) StartNotification() { + if n.entries == nil { + n.entries = make([]*log.Entry, 0, 10) + } +} + +func (n *discordTypeNotifier) SendNotification() { + if n.entries != nil && len(n.entries) != 0 { + n.sendEntries(n.entries) + } + n.entries = nil +} + +func (n *discordTypeNotifier) Levels() []log.Level { + // TODO: Make this configurable. + return []log.Level{ + log.PanicLevel, + log.FatalLevel, + log.ErrorLevel, + log.WarnLevel, + log.InfoLevel, + } +} + +func (n *discordTypeNotifier) Fire(entry *log.Entry) error { + if n.entries != nil { + n.entries = append(n.entries, entry) + } else { + // Log output generated outside a cycle is sent immediately. + n.sendEntries([]*log.Entry{entry}) + } + return nil +} + +type discordWebhookBody struct { + Content string `json:"content"` +} diff --git a/notifications/notifier.go b/notifications/notifier.go index a73a63e..854b31c 100644 --- a/notifications/notifier.go +++ b/notifications/notifier.go @@ -26,6 +26,8 @@ func NewNotifier(c *cli.Context) *Notifier { switch t { case emailType: tn = newEmailNotifier(c) + case discordType: + tn = newDiscordNotifier(c) default: log.Fatalf("Unknown notification type %q", t) }