mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-21 21:30:48 +02:00
parent
3bbe1bd109
commit
35490c853d
9 changed files with 375 additions and 295 deletions
|
@ -34,7 +34,11 @@ var (
|
||||||
scope string
|
scope string
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = NewRootCommand()
|
||||||
|
|
||||||
|
// NewRootCommand creates the root command for watchtower
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
Use: "watchtower",
|
Use: "watchtower",
|
||||||
Short: "Automatically updates running Docker containers",
|
Short: "Automatically updates running Docker containers",
|
||||||
Long: `
|
Long: `
|
||||||
|
@ -44,6 +48,7 @@ More information available at https://github.com/containrrr/watchtower/.
|
||||||
Run: Run,
|
Run: Run,
|
||||||
PreRun: PreRun,
|
PreRun: PreRun,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flags.SetDefaults()
|
flags.SetDefaults()
|
||||||
|
|
|
@ -1,29 +1,22 @@
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"net/smtp"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
shoutrrrSmtp "github.com/containrrr/shoutrrr/pkg/services/smtp"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
emailType = "email"
|
emailType = "email"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implements Notifier, 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 {
|
type emailTypeNotifier struct {
|
||||||
|
url string
|
||||||
From, To string
|
From, To string
|
||||||
Server, User, Password, SubjectTag string
|
Server, User, Password, SubjectTag string
|
||||||
Port int
|
Port int
|
||||||
|
@ -33,7 +26,12 @@ type emailTypeNotifier struct {
|
||||||
delay time.Duration
|
delay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
// NewEmailNotifier is a factory method creating a new email notifier instance
|
||||||
|
func NewEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier {
|
||||||
|
return newEmailNotifier(c, acceptedLogLevels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier {
|
||||||
flags := c.PersistentFlags()
|
flags := c.PersistentFlags()
|
||||||
|
|
||||||
from, _ := flags.GetString("notification-email-from")
|
from, _ := flags.GetString("notification-email-from")
|
||||||
|
@ -47,6 +45,7 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie
|
||||||
subjecttag, _ := flags.GetString("notification-email-subjecttag")
|
subjecttag, _ := flags.GetString("notification-email-subjecttag")
|
||||||
|
|
||||||
n := &emailTypeNotifier{
|
n := &emailTypeNotifier{
|
||||||
|
entries: []*log.Entry{},
|
||||||
From: from,
|
From: from,
|
||||||
To: to,
|
To: to,
|
||||||
Server: server,
|
Server: server,
|
||||||
|
@ -59,12 +58,33 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie
|
||||||
SubjectTag: subjecttag,
|
SubjectTag: subjecttag,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.AddHook(n)
|
|
||||||
|
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
|
func (e *emailTypeNotifier) GetURL() string {
|
||||||
|
conf := &shoutrrrSmtp.Config{
|
||||||
|
FromAddress: e.From,
|
||||||
|
FromName: "Watchtower",
|
||||||
|
ToAddresses: []string{e.To},
|
||||||
|
Port: uint16(e.Port),
|
||||||
|
Host: e.Server,
|
||||||
|
Subject: e.getSubject(),
|
||||||
|
Username: e.User,
|
||||||
|
Password: e.Password,
|
||||||
|
UseStartTLS: true,
|
||||||
|
UseHTML: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.User) > 0 {
|
||||||
|
conf.Set("auth", "Plain")
|
||||||
|
} else {
|
||||||
|
conf.Set("auth", "None")
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf.GetURL().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *emailTypeNotifier) getSubject() string {
|
||||||
var emailSubject string
|
var emailSubject string
|
||||||
|
|
||||||
if e.SubjectTag == "" {
|
if e.SubjectTag == "" {
|
||||||
|
@ -75,83 +95,13 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
|
||||||
if hostname, err := os.Hostname(); err == nil {
|
if hostname, err := os.Hostname(); err == nil {
|
||||||
emailSubject += " on " + hostname
|
emailSubject += " on " + hostname
|
||||||
}
|
}
|
||||||
body := ""
|
return emailSubject
|
||||||
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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Now()
|
// TODO: Delete these once all notifiers have been converted to shoutrrr
|
||||||
|
func (e *emailTypeNotifier) StartNotification() {}
|
||||||
header := make(map[string]string)
|
func (e *emailTypeNotifier) SendNotification() {}
|
||||||
header["From"] = e.From
|
func (e *emailTypeNotifier) Levels() []log.Level { return nil }
|
||||||
header["To"] = e.To
|
func (e *emailTypeNotifier) Fire(entry *log.Entry) error { return nil }
|
||||||
header["Subject"] = emailSubject
|
|
||||||
header["Date"] = t.Format(time.RFC1123Z)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
encodedBody := base64.StdEncoding.EncodeToString([]byte(body))
|
|
||||||
//RFC 2045 base64 encoding demands line no longer than 76 characters.
|
|
||||||
for _, line := range SplitSubN(encodedBody, 76) {
|
|
||||||
message += "\r\n" + line
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
if e.delay > 0 {
|
|
||||||
time.Sleep(e.delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
var auth smtp.Auth
|
|
||||||
if e.User != "" {
|
|
||||||
auth = smtp.PlainAuth("", e.User, e.Password, e.Server)
|
|
||||||
}
|
|
||||||
err := SendMail(e.Server+":"+strconv.Itoa(e.Port), e.tlsSkipVerify, auth, e.From, strings.Split(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 || len(e.entries) <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.sendEntries(e.entries)
|
|
||||||
e.entries = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *emailTypeNotifier) Levels() []log.Level {
|
|
||||||
return e.logLevels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *emailTypeNotifier) Fire(entry *log.Entry) error {
|
|
||||||
if e.entries != nil {
|
|
||||||
e.entries = append(e.entries, entry)
|
|
||||||
} else {
|
|
||||||
e.sendEntries([]*log.Entry{entry})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *emailTypeNotifier) Close() {}
|
func (e *emailTypeNotifier) Close() {}
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
shoutrrrGotify "github.com/containrrr/shoutrrr/pkg/services/gotify"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -24,10 +21,40 @@ type gotifyTypeNotifier struct {
|
||||||
logLevels []log.Level
|
logLevels []log.Level
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
// NewGotifyNotifier is a factory method creating a new gotify notifier instance
|
||||||
|
func NewGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier {
|
||||||
|
return newGotifyNotifier(c, levels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier {
|
||||||
flags := c.PersistentFlags()
|
flags := c.PersistentFlags()
|
||||||
|
|
||||||
|
url := getGotifyURL(flags)
|
||||||
|
token := getGotifyToken(flags)
|
||||||
|
|
||||||
|
skipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify")
|
||||||
|
|
||||||
|
n := &gotifyTypeNotifier{
|
||||||
|
gotifyURL: url,
|
||||||
|
gotifyAppToken: token,
|
||||||
|
gotifyInsecureSkipVerify: skipVerify,
|
||||||
|
logLevels: levels,
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGotifyToken(flags *pflag.FlagSet) string {
|
||||||
|
gotifyToken, _ := flags.GetString("notification-gotify-token")
|
||||||
|
if len(gotifyToken) < 1 {
|
||||||
|
log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.")
|
||||||
|
}
|
||||||
|
return gotifyToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGotifyURL(flags *pflag.FlagSet) string {
|
||||||
gotifyURL, _ := flags.GetString("notification-gotify-url")
|
gotifyURL, _ := flags.GetString("notification-gotify-url")
|
||||||
|
|
||||||
if len(gotifyURL) < 1 {
|
if len(gotifyURL) < 1 {
|
||||||
log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.")
|
log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.")
|
||||||
} else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) {
|
} else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) {
|
||||||
|
@ -36,82 +63,29 @@ func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifi
|
||||||
log.Warn("Using an HTTP url for Gotify is insecure")
|
log.Warn("Using an HTTP url for Gotify is insecure")
|
||||||
}
|
}
|
||||||
|
|
||||||
gotifyToken, _ := flags.GetString("notification-gotify-token")
|
return gotifyURL
|
||||||
if len(gotifyToken) < 1 {
|
|
||||||
log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gotifyInsecureSkipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify")
|
func (n *gotifyTypeNotifier) GetURL() string {
|
||||||
|
url := n.gotifyURL
|
||||||
|
|
||||||
n := &gotifyTypeNotifier{
|
if strings.HasPrefix(url, "https://") {
|
||||||
gotifyURL: gotifyURL,
|
url = strings.TrimPrefix(url, "https://")
|
||||||
gotifyAppToken: gotifyToken,
|
} else {
|
||||||
gotifyInsecureSkipVerify: gotifyInsecureSkipVerify,
|
url = strings.TrimPrefix(url, "http://")
|
||||||
logLevels: acceptedLogLevels,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.AddHook(n)
|
url = strings.TrimSuffix(url, "/")
|
||||||
|
|
||||||
return n
|
config := &shoutrrrGotify.Config{
|
||||||
|
Host: url,
|
||||||
|
Token: n.gotifyAppToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.GetURL().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *gotifyTypeNotifier) StartNotification() {}
|
func (n *gotifyTypeNotifier) StartNotification() {}
|
||||||
|
|
||||||
func (n *gotifyTypeNotifier) SendNotification() {}
|
func (n *gotifyTypeNotifier) SendNotification() {}
|
||||||
|
|
||||||
func (n *gotifyTypeNotifier) Close() {}
|
func (n *gotifyTypeNotifier) Close() {}
|
||||||
|
func (n *gotifyTypeNotifier) Levels() []log.Level { return nil }
|
||||||
func (n *gotifyTypeNotifier) Levels() []log.Level {
|
|
||||||
return n.logLevels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *gotifyTypeNotifier) getURL() string {
|
|
||||||
url := n.gotifyURL
|
|
||||||
if !strings.HasSuffix(url, "/") {
|
|
||||||
url += "/"
|
|
||||||
}
|
|
||||||
return url + "message?token=" + n.gotifyAppToken
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *gotifyTypeNotifier) Fire(entry *log.Entry) error {
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
jsonBody, err := json.Marshal(gotifyMessage{
|
|
||||||
Message: "(" + entry.Level.String() + "): " + entry.Message,
|
|
||||||
Title: "Watchtower",
|
|
||||||
Priority: 0,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to create JSON body for Gotify notification: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly define the client so we can set InsecureSkipVerify to the desired value.
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: n.gotifyInsecureSkipVerify,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
jsonBodyBuffer := bytes.NewBuffer([]byte(jsonBody))
|
|
||||||
resp, err := client.Post(n.getURL(), "application/json", jsonBodyBuffer)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to send Gotify notification: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
fmt.Printf("Gotify notification returned %d HTTP status code", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type gotifyMessage struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Priority int `json:"priority"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"strings"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
|
shoutrrrTeams "github.com/containrrr/shoutrrr/pkg/services/teams"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"io/ioutil"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -22,7 +19,12 @@ type msTeamsTypeNotifier struct {
|
||||||
data bool
|
data bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
// NewMsTeamsNotifier is a factory method creating a new teams notifier instance
|
||||||
|
func NewMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier {
|
||||||
|
return newMsTeamsNotifier(cmd, acceptedLogLevels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier {
|
||||||
|
|
||||||
flags := cmd.PersistentFlags()
|
flags := cmd.PersistentFlags()
|
||||||
|
|
||||||
|
@ -38,103 +40,29 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Not
|
||||||
data: withData,
|
data: withData,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.AddHook(n)
|
|
||||||
|
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *msTeamsTypeNotifier) GetURL() string {
|
||||||
|
|
||||||
|
baseURL := "https://outlook.office.com/webhook/"
|
||||||
|
|
||||||
|
path := strings.Replace(n.webHookURL, baseURL, "", 1)
|
||||||
|
rawToken := strings.Replace(path, "/IncomingWebhook", "", 1)
|
||||||
|
token := strings.Split(rawToken, "/")
|
||||||
|
config := &shoutrrrTeams.Config{
|
||||||
|
Token: shoutrrrTeams.Token{
|
||||||
|
A: token[0],
|
||||||
|
B: token[1],
|
||||||
|
C: token[2],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.GetURL().String()
|
||||||
|
}
|
||||||
|
|
||||||
func (n *msTeamsTypeNotifier) StartNotification() {}
|
func (n *msTeamsTypeNotifier) StartNotification() {}
|
||||||
|
|
||||||
func (n *msTeamsTypeNotifier) SendNotification() {}
|
func (n *msTeamsTypeNotifier) SendNotification() {}
|
||||||
|
|
||||||
func (n *msTeamsTypeNotifier) Close() {}
|
func (n *msTeamsTypeNotifier) Close() {}
|
||||||
|
func (n *msTeamsTypeNotifier) Levels() []log.Level { return nil }
|
||||||
func (n *msTeamsTypeNotifier) Levels() []log.Level {
|
func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { return nil }
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,26 +31,48 @@ func NewNotifier(c *cobra.Command) *Notifier {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal()
|
log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n.types = n.GetNotificationTypes(c, acceptedLogLevels, types)
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationTypes produces an array of notifiers from a list of types
|
||||||
|
func (n *Notifier) GetNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier {
|
||||||
|
output := make([]ty.Notifier, 0)
|
||||||
|
|
||||||
for _, t := range types {
|
for _, t := range types {
|
||||||
var tn ty.Notifier
|
|
||||||
|
if t == shoutrrrType {
|
||||||
|
output = append(output, newShoutrrrNotifier(cmd, levels))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacyNotifier ty.ConvertableNotifier
|
||||||
|
|
||||||
switch t {
|
switch t {
|
||||||
case emailType:
|
case emailType:
|
||||||
tn = newEmailNotifier(c, acceptedLogLevels)
|
legacyNotifier = newEmailNotifier(cmd, []log.Level{})
|
||||||
case slackType:
|
case slackType:
|
||||||
tn = newSlackNotifier(c, acceptedLogLevels)
|
legacyNotifier = newSlackNotifier(cmd, []log.Level{})
|
||||||
case msTeamsType:
|
case msTeamsType:
|
||||||
tn = newMsTeamsNotifier(c, acceptedLogLevels)
|
legacyNotifier = newMsTeamsNotifier(cmd, levels)
|
||||||
case gotifyType:
|
case gotifyType:
|
||||||
tn = newGotifyNotifier(c, acceptedLogLevels)
|
legacyNotifier = newGotifyNotifier(cmd, []log.Level{})
|
||||||
case shoutrrrType:
|
|
||||||
tn = newShoutrrrNotifier(c, acceptedLogLevels)
|
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Unknown notification type %q", t)
|
log.Fatalf("Unknown notification type %q", t)
|
||||||
}
|
}
|
||||||
n.types = append(n.types, tn)
|
|
||||||
|
notifier := newShoutrrrNotifierFromURL(
|
||||||
|
cmd,
|
||||||
|
legacyNotifier.GetURL(),
|
||||||
|
levels,
|
||||||
|
)
|
||||||
|
|
||||||
|
output = append(output, notifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
return n
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartNotification starts a log batch. Notifications will be accumulated after this point and only sent when SendNotification() is called.
|
// StartNotification starts a log batch. Notifications will be accumulated after this point and only sent when SendNotification() is called.
|
||||||
|
|
163
pkg/notifications/notifier_test.go
Normal file
163
pkg/notifications/notifier_test.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package notifications_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/cmd"
|
||||||
|
"github.com/containrrr/watchtower/internal/flags"
|
||||||
|
"github.com/containrrr/watchtower/pkg/notifications"
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActions(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Notifier Suite")
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("notifications", func() {
|
||||||
|
// TODO: Either, we delete this test or we need to pass it valid URLs in the cobra command.
|
||||||
|
// ---
|
||||||
|
// When("getting notifiers from a types array", func() {
|
||||||
|
// It("should return the same amount of notifiers a string entries", func() {
|
||||||
|
|
||||||
|
// notifier := ¬ifications.Notifier{}
|
||||||
|
// notifiers := notifier.GetNotificationTypes(&cobra.Command{}, []log.Level{}, []string{"slack", "email"})
|
||||||
|
// Expect(len(notifiers)).To(Equal(2))
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
Describe("the slack notifier", func() {
|
||||||
|
When("converting a slack service config into a shoutrrr url", func() {
|
||||||
|
builderFn := notifications.NewSlackNotifier
|
||||||
|
|
||||||
|
It("should return the expected URL", func() {
|
||||||
|
|
||||||
|
username := "containrrrbot"
|
||||||
|
tokenA := "aaa"
|
||||||
|
tokenB := "bbb"
|
||||||
|
tokenC := "ccc"
|
||||||
|
|
||||||
|
password := fmt.Sprintf("%s-%s-%s", tokenA, tokenB, tokenC)
|
||||||
|
hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC)
|
||||||
|
expectedOutput := fmt.Sprintf("slack://%s:%s@%s/%s/%s", username, password, tokenA, tokenB, tokenC)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--notification-slack-hook-url",
|
||||||
|
hookURL,
|
||||||
|
"--notification-slack-identifier",
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
|
||||||
|
testURL(builderFn, args, expectedOutput)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("the gotify notifier", func() {
|
||||||
|
When("converting a gotify service config into a shoutrrr url", func() {
|
||||||
|
builderFn := notifications.NewGotifyNotifier
|
||||||
|
|
||||||
|
It("should return the expected URL", func() {
|
||||||
|
token := "aaa"
|
||||||
|
host := "shoutrrr.local"
|
||||||
|
|
||||||
|
expectedOutput := fmt.Sprintf("gotify://%s/%s", host, token)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--notification-gotify-url",
|
||||||
|
fmt.Sprintf("https://%s", host),
|
||||||
|
"--notification-gotify-token",
|
||||||
|
token,
|
||||||
|
}
|
||||||
|
|
||||||
|
testURL(builderFn, args, expectedOutput)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("the teams notifier", func() {
|
||||||
|
When("converting a teams service config into a shoutrrr url", func() {
|
||||||
|
builderFn := notifications.NewMsTeamsNotifier
|
||||||
|
|
||||||
|
It("should return the expected URL", func() {
|
||||||
|
|
||||||
|
tokenA := "aaa"
|
||||||
|
tokenB := "bbb"
|
||||||
|
tokenC := "ccc"
|
||||||
|
|
||||||
|
hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC)
|
||||||
|
expectedOutput := fmt.Sprintf("teams://%s:%s@%s", tokenA, tokenB, tokenC)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--notification-msteams-hook",
|
||||||
|
hookURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
testURL(builderFn, args, expectedOutput)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("the email notifier", func() {
|
||||||
|
|
||||||
|
builderFn := notifications.NewEmailNotifier
|
||||||
|
|
||||||
|
When("converting an email service config into a shoutrrr url", func() {
|
||||||
|
It("should set the from address in the URL", func() {
|
||||||
|
fromAddress := "lala@example.com"
|
||||||
|
expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, "", "None")
|
||||||
|
args := []string{
|
||||||
|
"--notification-email-from",
|
||||||
|
fromAddress,
|
||||||
|
}
|
||||||
|
testURL(builderFn, args, expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return the expected URL", func() {
|
||||||
|
|
||||||
|
fromAddress := "sender@example.com"
|
||||||
|
toAddress := "receiver@example.com"
|
||||||
|
expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, toAddress, "None")
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--notification-email-from",
|
||||||
|
fromAddress,
|
||||||
|
"--notification-email-to",
|
||||||
|
toAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
testURL(builderFn, args, expectedOutput)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
subject := fmt.Sprintf("Watchtower updates on %s", hostname)
|
||||||
|
|
||||||
|
var template = "smtp://%s:%s@%s:%d/?fromAddress=%s&fromName=Watchtower&toAddresses=%s&auth=%s&subject=%s&startTls=Yes&useHTML=No"
|
||||||
|
return fmt.Sprintf(template, username, password, host, port, from, to, auth, subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
type builderFn = func(c *cobra.Command, acceptedLogLevels []log.Level) types.ConvertableNotifier
|
||||||
|
|
||||||
|
func testURL(builder builderFn, args []string, expectedURL string) {
|
||||||
|
|
||||||
|
command := cmd.NewRootCommand()
|
||||||
|
flags.RegisterNotificationFlags(command)
|
||||||
|
command.ParseFlags(args)
|
||||||
|
|
||||||
|
notifier := builder(command, []log.Level{})
|
||||||
|
actualURL := notifier.GetURL()
|
||||||
|
|
||||||
|
Expect(actualURL).To(Equal(expectedURL))
|
||||||
|
}
|
|
@ -35,8 +35,17 @@ type shoutrrrTypeNotifier struct {
|
||||||
|
|
||||||
func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
||||||
flags := c.PersistentFlags()
|
flags := c.PersistentFlags()
|
||||||
|
|
||||||
urls, _ := flags.GetStringArray("notification-url")
|
urls, _ := flags.GetStringArray("notification-url")
|
||||||
|
template := getShoutrrrTemplate(c)
|
||||||
|
return createSender(urls, acceptedLogLevels, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newShoutrrrNotifierFromURL(c *cobra.Command, url string, levels []log.Level) t.Notifier {
|
||||||
|
template := getShoutrrrTemplate(c)
|
||||||
|
return createSender([]string{url}, levels, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSender(urls []string, levels []log.Level, template *template.Template) t.Notifier {
|
||||||
r, err := shoutrrr.CreateSender(urls...)
|
r, err := shoutrrr.CreateSender(urls...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
|
log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
|
||||||
|
@ -45,10 +54,10 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti
|
||||||
n := &shoutrrrTypeNotifier{
|
n := &shoutrrrTypeNotifier{
|
||||||
Urls: urls,
|
Urls: urls,
|
||||||
Router: r,
|
Router: r,
|
||||||
logLevels: acceptedLogLevels,
|
|
||||||
template: getShoutrrrTemplate(c),
|
|
||||||
messages: make(chan string, 1),
|
messages: make(chan string, 1),
|
||||||
done: make(chan bool),
|
done: make(chan bool),
|
||||||
|
logLevels: levels,
|
||||||
|
template: template,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.AddHook(n)
|
log.AddHook(n)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
"github.com/johntdyer/slackrus"
|
"github.com/johntdyer/slackrus"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -15,7 +18,12 @@ type slackTypeNotifier struct {
|
||||||
slackrus.SlackrusHook
|
slackrus.SlackrusHook
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
// NewSlackNotifier is a factory function used to generate new instance of the slack notifier type
|
||||||
|
func NewSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier {
|
||||||
|
return newSlackNotifier(c, acceptedLogLevels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier {
|
||||||
flags := c.PersistentFlags()
|
flags := c.PersistentFlags()
|
||||||
|
|
||||||
hookURL, _ := flags.GetString("notification-slack-hook-url")
|
hookURL, _ := flags.GetString("notification-slack-hook-url")
|
||||||
|
@ -23,7 +31,6 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie
|
||||||
channel, _ := flags.GetString("notification-slack-channel")
|
channel, _ := flags.GetString("notification-slack-channel")
|
||||||
emoji, _ := flags.GetString("notification-slack-icon-emoji")
|
emoji, _ := flags.GetString("notification-slack-icon-emoji")
|
||||||
iconURL, _ := flags.GetString("notification-slack-icon-url")
|
iconURL, _ := flags.GetString("notification-slack-icon-url")
|
||||||
|
|
||||||
n := &slackTypeNotifier{
|
n := &slackTypeNotifier{
|
||||||
SlackrusHook: slackrus.SlackrusHook{
|
SlackrusHook: slackrus.SlackrusHook{
|
||||||
HookURL: hookURL,
|
HookURL: hookURL,
|
||||||
|
@ -34,12 +41,27 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie
|
||||||
AcceptedLevels: acceptedLogLevels,
|
AcceptedLevels: acceptedLogLevels,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
log.AddHook(n)
|
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *slackTypeNotifier) StartNotification() {}
|
func (s *slackTypeNotifier) GetURL() string {
|
||||||
|
rawTokens := strings.Replace(s.HookURL, "https://hooks.slack.com/services/", "", 1)
|
||||||
|
tokens := strings.Split(rawTokens, "/")
|
||||||
|
|
||||||
|
conf := &shoutrrrSlack.Config{
|
||||||
|
BotName: s.Username,
|
||||||
|
Token: shoutrrrSlack.Token{
|
||||||
|
A: tokens[0],
|
||||||
|
B: tokens[1],
|
||||||
|
C: tokens[2],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf.GetURL().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *slackTypeNotifier) StartNotification() {
|
||||||
|
}
|
||||||
|
|
||||||
func (s *slackTypeNotifier) SendNotification() {}
|
func (s *slackTypeNotifier) SendNotification() {}
|
||||||
|
|
||||||
|
|
7
pkg/types/convertable_notifier.go
Normal file
7
pkg/types/convertable_notifier.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
// ConvertableNotifier is a notifier capable of creating a shoutrrr URL
|
||||||
|
type ConvertableNotifier interface {
|
||||||
|
Notifier
|
||||||
|
GetURL() string
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue