You've already forked watchtower
							
							
				mirror of
				https://github.com/containrrr/watchtower.git
				synced 2025-10-31 00:17:44 +02:00 
			
		
		
		
	
							
								
								
									
										13
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -34,15 +34,20 @@ var ( | ||||
| 	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", | ||||
| 		Short: "Automatically updates running Docker containers", | ||||
| 		Long: ` | ||||
| Watchtower automatically updates running Docker containers whenever a new image is released. | ||||
| More information available at https://github.com/containrrr/watchtower/. | ||||
| `, | ||||
| 	Watchtower automatically updates running Docker containers whenever a new image is released. | ||||
| 	More information available at https://github.com/containrrr/watchtower/. | ||||
| 	`, | ||||
| 		Run:    Run, | ||||
| 		PreRun: PreRun, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func init() { | ||||
|   | ||||
| @@ -1,29 +1,22 @@ | ||||
| package notifications | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"net/smtp" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/spf13/cobra" | ||||
|  | ||||
| 	shoutrrrSmtp "github.com/containrrr/shoutrrr/pkg/services/smtp" | ||||
| 	t "github.com/containrrr/watchtower/pkg/types" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"strconv" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	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 { | ||||
| 	url                                string | ||||
| 	From, To                           string | ||||
| 	Server, User, Password, SubjectTag string | ||||
| 	Port                               int | ||||
| @@ -33,7 +26,12 @@ type emailTypeNotifier struct { | ||||
| 	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() | ||||
|  | ||||
| 	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") | ||||
|  | ||||
| 	n := &emailTypeNotifier{ | ||||
| 		entries:       []*log.Entry{}, | ||||
| 		From:          from, | ||||
| 		To:            to, | ||||
| 		Server:        server, | ||||
| @@ -59,12 +58,33 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie | ||||
| 		SubjectTag:    subjecttag, | ||||
| 	} | ||||
|  | ||||
| 	log.AddHook(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 | ||||
|  | ||||
| 	if e.SubjectTag == "" { | ||||
| @@ -75,83 +95,13 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { | ||||
| 	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. | ||||
| 	} | ||||
|  | ||||
| 	t := time.Now() | ||||
|  | ||||
| 	header := make(map[string]string) | ||||
| 	header["From"] = e.From | ||||
| 	header["To"] = e.To | ||||
| 	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) | ||||
| 	return emailSubject | ||||
| } | ||||
|  | ||||
| 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 | ||||
| } | ||||
| // TODO: Delete these once all notifiers have been converted to shoutrrr | ||||
| func (e *emailTypeNotifier) StartNotification()          {} | ||||
| func (e *emailTypeNotifier) SendNotification()           {} | ||||
| func (e *emailTypeNotifier) Levels() []log.Level         { return nil } | ||||
| func (e *emailTypeNotifier) Fire(entry *log.Entry) error { return nil } | ||||
|  | ||||
| func (e *emailTypeNotifier) Close() {} | ||||
|   | ||||
| @@ -1,16 +1,13 @@ | ||||
| package notifications | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	shoutrrrGotify "github.com/containrrr/shoutrrr/pkg/services/gotify" | ||||
| 	t "github.com/containrrr/watchtower/pkg/types" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/pflag" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -24,10 +21,40 @@ type gotifyTypeNotifier struct { | ||||
| 	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() | ||||
|  | ||||
| 	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") | ||||
|  | ||||
| 	if len(gotifyURL) < 1 { | ||||
| 		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://")) { | ||||
| @@ -36,82 +63,29 @@ func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifi | ||||
| 		log.Warn("Using an HTTP url for Gotify is insecure") | ||||
| 	} | ||||
|  | ||||
| 	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 gotifyURL | ||||
| } | ||||
|  | ||||
| func (n *gotifyTypeNotifier) GetURL() string { | ||||
| 	url := n.gotifyURL | ||||
|  | ||||
| 	if strings.HasPrefix(url, "https://") { | ||||
| 		url = strings.TrimPrefix(url, "https://") | ||||
| 	} else { | ||||
| 		url = strings.TrimPrefix(url, "http://") | ||||
| 	} | ||||
|  | ||||
| 	gotifyInsecureSkipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") | ||||
| 	url = strings.TrimSuffix(url, "/") | ||||
|  | ||||
| 	n := &gotifyTypeNotifier{ | ||||
| 		gotifyURL:                gotifyURL, | ||||
| 		gotifyAppToken:           gotifyToken, | ||||
| 		gotifyInsecureSkipVerify: gotifyInsecureSkipVerify, | ||||
| 		logLevels:                acceptedLogLevels, | ||||
| 	config := &shoutrrrGotify.Config{ | ||||
| 		Host:  url, | ||||
| 		Token: n.gotifyAppToken, | ||||
| 	} | ||||
|  | ||||
| 	log.AddHook(n) | ||||
|  | ||||
| 	return n | ||||
| 	return config.GetURL().String() | ||||
| } | ||||
|  | ||||
| func (n *gotifyTypeNotifier) StartNotification()  {} | ||||
|  | ||||
| func (n *gotifyTypeNotifier) SendNotification()   {} | ||||
|  | ||||
| func (n *gotifyTypeNotifier) Close()              {} | ||||
|  | ||||
| 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"` | ||||
| } | ||||
| func (n *gotifyTypeNotifier) Levels() []log.Level { return nil } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| package notifications | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	shoutrrrTeams "github.com/containrrr/shoutrrr/pkg/services/teams" | ||||
| 	t "github.com/containrrr/watchtower/pkg/types" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"io/ioutil" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -22,7 +19,12 @@ type msTeamsTypeNotifier struct { | ||||
| 	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() | ||||
|  | ||||
| @@ -38,103 +40,29 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Not | ||||
| 		data:       withData, | ||||
| 	} | ||||
|  | ||||
| 	log.AddHook(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) SendNotification()           {} | ||||
|  | ||||
| func (n *msTeamsTypeNotifier) Close()                      {} | ||||
|  | ||||
| func (n *msTeamsTypeNotifier) Levels() []log.Level { | ||||
| 	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"` | ||||
| } | ||||
| func (n *msTeamsTypeNotifier) Levels() []log.Level         { return nil } | ||||
| func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { return nil } | ||||
|   | ||||
| @@ -31,26 +31,48 @@ func NewNotifier(c *cobra.Command) *Notifier { | ||||
| 	if err != nil { | ||||
| 		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 { | ||||
| 		var tn ty.Notifier | ||||
|  | ||||
| 		if t == shoutrrrType { | ||||
| 			output = append(output, newShoutrrrNotifier(cmd, levels)) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		var legacyNotifier ty.ConvertableNotifier | ||||
|  | ||||
| 		switch t { | ||||
| 		case emailType: | ||||
| 			tn = newEmailNotifier(c, acceptedLogLevels) | ||||
| 			legacyNotifier = newEmailNotifier(cmd, []log.Level{}) | ||||
| 		case slackType: | ||||
| 			tn = newSlackNotifier(c, acceptedLogLevels) | ||||
| 			legacyNotifier = newSlackNotifier(cmd, []log.Level{}) | ||||
| 		case msTeamsType: | ||||
| 			tn = newMsTeamsNotifier(c, acceptedLogLevels) | ||||
| 			legacyNotifier = newMsTeamsNotifier(cmd, levels) | ||||
| 		case gotifyType: | ||||
| 			tn = newGotifyNotifier(c, acceptedLogLevels) | ||||
| 		case shoutrrrType: | ||||
| 			tn = newShoutrrrNotifier(c, acceptedLogLevels) | ||||
| 			legacyNotifier = newGotifyNotifier(cmd, []log.Level{}) | ||||
| 		default: | ||||
| 			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. | ||||
|   | ||||
							
								
								
									
										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 { | ||||
| 	flags := c.PersistentFlags() | ||||
|  | ||||
| 	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...) | ||||
| 	if err != nil { | ||||
| 		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{ | ||||
| 		Urls:      urls, | ||||
| 		Router:    r, | ||||
| 		logLevels: acceptedLogLevels, | ||||
| 		template:  getShoutrrrTemplate(c), | ||||
| 		messages:  make(chan string, 1), | ||||
| 		done:      make(chan bool), | ||||
| 		logLevels: levels, | ||||
| 		template:  template, | ||||
| 	} | ||||
|  | ||||
| 	log.AddHook(n) | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| package notifications | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack" | ||||
| 	t "github.com/containrrr/watchtower/pkg/types" | ||||
| 	"github.com/johntdyer/slackrus" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| @@ -15,7 +18,12 @@ type slackTypeNotifier struct { | ||||
| 	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() | ||||
|  | ||||
| 	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") | ||||
| 	emoji, _ := flags.GetString("notification-slack-icon-emoji") | ||||
| 	iconURL, _ := flags.GetString("notification-slack-icon-url") | ||||
|  | ||||
| 	n := &slackTypeNotifier{ | ||||
| 		SlackrusHook: slackrus.SlackrusHook{ | ||||
| 			HookURL:        hookURL, | ||||
| @@ -34,12 +41,27 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie | ||||
| 			AcceptedLevels: acceptedLogLevels, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	log.AddHook(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() {} | ||||
|  | ||||
|   | ||||
							
								
								
									
										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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user