From 5adb143f628fbfe2e16fd29349d4eb541c01fa0e Mon Sep 17 00:00:00 2001 From: Rodrigo Damazio Bovendorp Date: Sun, 29 Oct 2017 23:30:44 -0700 Subject: [PATCH] Adding basic (but flexible) notification system which hooks into logrus. This only adds e-mail notifications, but others could be easily done. In many cases, adding another existing logrus hook will be sufficient. --- main.go | 40 ++++++++++++- notifications/email.go | 115 ++++++++++++++++++++++++++++++++++++++ notifications/notifier.go | 46 +++++++++++++++ 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 notifications/email.go create mode 100644 notifications/notifier.go diff --git a/main.go b/main.go index e97c4d8..0cf3465 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main // import "github.com/v2tec/watchtower" import ( - "fmt" "os" "os/signal" "syscall" @@ -14,6 +13,7 @@ import ( "github.com/urfave/cli" "github.com/v2tec/watchtower/actions" "github.com/v2tec/watchtower/container" + "github.com/v2tec/watchtower/notifications" ) // DockerAPIMinVersion is the version of the docker API, which is minimally required by @@ -27,6 +27,7 @@ var ( scheduleSpec string cleanup bool noRestart bool + notifier *notifications.Notifier ) func init() { @@ -82,6 +83,37 @@ func main() { Name: "debug", Usage: "enable debug mode with verbose logging", }, + cli.StringSliceFlag{ + Name: "notifications", + Value: &cli.StringSlice{}, + Usage: "notification types to send (valid: email)", + EnvVar: "WATCHTOWER_NOTIFICATIONS", + }, + cli.StringFlag{ + 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", + EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_TO", + }, + cli.StringFlag{ + Name: "notification-email-server", + Usage: "SMTP server to send notification e-mails through", + EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER", + }, + cli.StringFlag{ + 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", + EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", + }, } if err := app.Run(os.Args); err != nil { @@ -115,6 +147,8 @@ func before(c *cli.Context) error { } client = container.NewClient(!c.GlobalBool("no-pull")) + notifier = notifications.NewNotifier(c) + return nil } @@ -135,9 +169,11 @@ func start(c *cli.Context) error { select { case v := <-tryLockSem: defer func() { tryLockSem <- v }() + notifier.StartNotification() if err := actions.Update(client, names, cleanup, noRestart); err != nil { - fmt.Println(err) + log.Println(err) } + notifier.SendNotification() default: log.Debug("Skipped another update already running.") } diff --git a/notifications/email.go b/notifications/email.go new file mode 100644 index 0000000..9fd8774 --- /dev/null +++ b/notifications/email.go @@ -0,0 +1,115 @@ +package notifications + +import ( + "encoding/base64" + "fmt" + "net/smtp" + "os" + + log "github.com/Sirupsen/logrus" + "github.com/urfave/cli" +) + +const ( + emailType = "email" +) + +// Implements typeNotifier, logrus.Hook +// The default logrus email integration would have several issues: +// - It would send one email per log output +// - It would only send errors +// We work around that by holding on to log entries until the update cycle is done. +type emailTypeNotifier struct { + From, To string + Server, User, Password string + entries []*log.Entry +} + +func newEmailNotifier(c *cli.Context) typeNotifier { + n := &emailTypeNotifier{ + From: c.GlobalString("notification-email-from"), + To: c.GlobalString("notification-email-to"), + Server: c.GlobalString("notification-email-server"), + User: c.GlobalString("notification-email-server-user"), + Password: c.GlobalString("notification-email-server-password"), + } + + log.AddHook(n) + + return n +} + +func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { + emailSubject := "Watchtower updates" + if hostname, err := os.Hostname(); err == nil { + emailSubject += " on " + hostname + } + body := "" + for _, entry := range entries { + body += entry.Time.Format("2006-01-02 15:04:05") + " (" + entry.Level.String() + "): " + entry.Message + "\r\n" + // We don't use fields in watchtower, so don't bother sending them. + } + + header := make(map[string]string) + header["From"] = e.From + header["To"] = e.To + header["Subject"] = emailSubject + header["MIME-Version"] = "1.0" + header["Content-Type"] = "text/plain; charset=\"utf-8\"" + header["Content-Transfer-Encoding"] = "base64" + + message := "" + for k, v := range header { + message += fmt.Sprintf("%s: %s\r\n", k, v) + } + message += "\r\n" + base64.StdEncoding.EncodeToString([]byte(body)) + + return []byte(message) +} + +func (e *emailTypeNotifier) sendEntries(entries []*log.Entry) { + // Do the sending in a separate goroutine so we don't block the main process. + msg := e.buildMessage(entries) + go func() { + auth := smtp.PlainAuth("", e.User, e.Password, e.Server) + err := smtp.SendMail(e.Server+":25", auth, e.From, []string{e.To}, msg) + if err != nil { + // Use fmt so it doesn't trigger another email. + fmt.Println("Failed to send notification email: ", err) + } + }() +} + +func (e *emailTypeNotifier) StartNotification() { + if e.entries == nil { + e.entries = make([]*log.Entry, 0, 10) + } +} + +func (e *emailTypeNotifier) SendNotification() { + if e.entries != nil { + e.sendEntries(e.entries) + e.entries = nil + } +} + +func (e *emailTypeNotifier) Levels() []log.Level { + // TODO: Make this configurable. + return []log.Level{ + log.PanicLevel, + log.FatalLevel, + log.ErrorLevel, + log.WarnLevel, + log.InfoLevel, + } +} + +func (e *emailTypeNotifier) Fire(entry *log.Entry) error { + if e.entries != nil { + e.entries = append(e.entries, entry) + } else { + // Log output generated outside a cycle is sent immediately. + e.sendEntries([]*log.Entry{entry}) + } + return nil +} diff --git a/notifications/notifier.go b/notifications/notifier.go new file mode 100644 index 0000000..b7ec7a1 --- /dev/null +++ b/notifications/notifier.go @@ -0,0 +1,46 @@ +package notifications + +import ( + log "github.com/Sirupsen/logrus" + "github.com/urfave/cli" +) + +type typeNotifier interface { + StartNotification() + SendNotification() +} + +type Notifier struct { + types []typeNotifier +} + +func NewNotifier(c *cli.Context) *Notifier { + n := &Notifier{} + + // Parse types and create notifiers. + types := c.GlobalStringSlice("notifications") + for _, t := range types { + var tn typeNotifier + switch t { + case emailType: + tn = newEmailNotifier(c) + default: + log.Fatalf("Unknown notification type %q", t) + } + n.types = append(n.types, tn) + } + + return n +} + +func (n *Notifier) StartNotification() { + for _, t := range n.types { + t.StartNotification() + } +} + +func (n *Notifier) SendNotification() { + for _, t := range n.types { + t.SendNotification() + } +}