Merge pull request #106 from rdamazio/master

Adding basic (but flexible) notification system which hooks into logrus.
pull/104/head
stffabi 7 years ago committed by GitHub
commit f365014b8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -98,6 +98,8 @@ docker run --rm v2tec/watchtower --help
* `--debug` Enable debug mode. When this option is specified you'll see more verbose logging in the watchtower log file. * `--debug` Enable debug mode. When this option is specified you'll see more verbose logging in the watchtower log file.
* `--help` Show documentation about the supported flags. * `--help` Show documentation about the supported flags.
See below for options used to configure notifications.
## Linked Containers ## Linked Containers
Watchtower will detect if there are links between any of the running containers and ensure that things are stopped/started in a way that won't break any of the links. If an update is detected for one of the dependencies in a group of linked containers, watchtower will stop and start all of the containers in the correct order so that the application comes back up correctly. Watchtower will detect if there are links between any of the running containers and ensure that things are stopped/started in a way that won't break any of the links. If an update is detected for one of the dependencies in a group of linked containers, watchtower will stop and start all of the containers in the correct order so that the application comes back up correctly.
@ -161,3 +163,34 @@ docker run -d \
## Updating Watchtower ## Updating Watchtower
If watchtower is monitoring the same Docker daemon under which the watchtower container itself is running (i.e. if you volume-mounted */var/run/docker.sock* into the watchtower container) then it has the ability to update itself. If a new version of the *v2tec/watchtower* image is pushed to the Docker Hub, your watchtower will pull down the new image and restart itself automatically. If watchtower is monitoring the same Docker daemon under which the watchtower container itself is running (i.e. if you volume-mounted */var/run/docker.sock* into the watchtower container) then it has the ability to update itself. If a new version of the *v2tec/watchtower* image is pushed to the Docker Hub, your watchtower will pull down the new image and restart itself automatically.
## Notifications
Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging system, [logrus](http://github.com/sirupsen/logrus).
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
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.
* `--notification-email-to` (env. `WATCHTOWER_NOTIFICATION_EMAIL_TO`): The e-mail address to which notifications will be sent.
* `--notification-email-server` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER`): The SMTP server to send e-mails through.
* `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.
* `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with.
Example:
```bash
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_NOTIFICATIONS=email \
-e WATCHTOWER_NOTIFICATION_EMAIL_FROM=fromaddress@gmail.com \
-e WATCHTOWER_NOTIFICATION_EMAIL_TO=toaddress@gmail.com \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=fromaddress@gmail.com \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \
v2tec/watchtower
```

@ -1,7 +1,6 @@
package main // import "github.com/v2tec/watchtower" package main // import "github.com/v2tec/watchtower"
import ( import (
"fmt"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -14,6 +13,7 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
"github.com/v2tec/watchtower/actions" "github.com/v2tec/watchtower/actions"
"github.com/v2tec/watchtower/container" "github.com/v2tec/watchtower/container"
"github.com/v2tec/watchtower/notifications"
) )
// DockerAPIMinVersion is the version of the docker API, which is minimally required by // DockerAPIMinVersion is the version of the docker API, which is minimally required by
@ -27,6 +27,7 @@ var (
scheduleSpec string scheduleSpec string
cleanup bool cleanup bool
noRestart bool noRestart bool
notifier *notifications.Notifier
) )
func init() { func init() {
@ -82,6 +83,37 @@ func main() {
Name: "debug", Name: "debug",
Usage: "enable debug mode with verbose logging", 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 { 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")) client = container.NewClient(!c.GlobalBool("no-pull"))
notifier = notifications.NewNotifier(c)
return nil return nil
} }
@ -135,9 +169,11 @@ func start(c *cli.Context) error {
select { select {
case v := <-tryLockSem: case v := <-tryLockSem:
defer func() { tryLockSem <- v }() defer func() { tryLockSem <- v }()
notifier.StartNotification()
if err := actions.Update(client, names, cleanup, noRestart); err != nil { if err := actions.Update(client, names, cleanup, noRestart); err != nil {
fmt.Println(err) log.Println(err)
} }
notifier.SendNotification()
default: default:
log.Debug("Skipped another update already running.") log.Debug("Skipped another update already running.")
} }

@ -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
}

@ -0,0 +1,50 @@
package notifications
import (
log "github.com/Sirupsen/logrus"
"github.com/urfave/cli"
)
type typeNotifier interface {
StartNotification()
SendNotification()
}
// Notifier can send log output as notification to admins, with optional batching.
type Notifier struct {
types []typeNotifier
}
// NewNotifier creates and returns a new Notifier, using global configuration.
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
}
// StartNotification starts a log batch. Notifications will be accumulated after this point and only sent when SendNotification() is called.
func (n *Notifier) StartNotification() {
for _, t := range n.types {
t.StartNotification()
}
}
// SendNotification sends any notifications accumulated since StartNotification() was called.
func (n *Notifier) SendNotification() {
for _, t := range n.types {
t.SendNotification()
}
}
Loading…
Cancel
Save