You've already forked watchtower
							
							
				mirror of
				https://github.com/containrrr/watchtower.git
				synced 2025-10-31 00:17:44 +02:00 
			
		
		
		
	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.
This commit is contained in:
		
							
								
								
									
										40
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								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.") | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										115
									
								
								notifications/email.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								notifications/email.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										46
									
								
								notifications/notifier.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								notifications/notifier.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user