From 5b52b0755829771c3f0e103815435f3b4551f5f7 Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Fri, 18 Jun 2021 15:26:03 +1000 Subject: [PATCH 1/3] feat: allow hostname override for email notifier As it currently stands all notifiers utilise `os.Hostname` to populate their titles/subjects. When utilising Docker with a bridged network if you set the hostname for a container to an external DNS hostname Docker's internal DNS resolver will override said hostname for all containers within the bridged network. This change allows a user to specify what hostname should be represented in the email notifications without having to change the `os.Hostname`. --- docs/notifications.md | 1 + internal/flags/flags.go | 6 +++++ pkg/notifications/email.go | 42 ++++++++++++++++-------------- pkg/notifications/gotify.go | 2 +- pkg/notifications/msteams.go | 2 +- pkg/notifications/notifier.go | 10 ++++--- pkg/notifications/notifier_test.go | 8 +++--- pkg/notifications/slack.go | 4 +-- 8 files changed, 44 insertions(+), 31 deletions(-) diff --git a/docs/notifications.md b/docs/notifications.md index b0e2adf01..683063b9c 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -42,6 +42,7 @@ To receive notifications by email, the following command-line options, or their - `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used. - `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds. - `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. +- `--notification-email-subjecthostname` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTHOSTNAME`): Custom hostname specified in subject. Useful to override the hostname. Example: diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 177073df1..964c6b40d 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -240,6 +240,12 @@ Should only be used for testing.`) viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), "Subject prefix tag for notifications via mail") + flags.StringP( + "notification-email-subjecthostname", + "", + viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTHOSTNAME"), + "Subject custom hostname for notifications via mail") + flags.StringP( "notification-slack-hook-url", "", diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 4984139b1..5b954db4f 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -15,14 +15,14 @@ const ( ) type emailTypeNotifier struct { - url string - From, To string - Server, User, Password, SubjectTag string - Port int - tlsSkipVerify bool - entries []*log.Entry - logLevels []log.Level - delay time.Duration + url string + From, To string + Server, User, Password, SubjectTag, SubjectHostname string + Port int + tlsSkipVerify bool + entries []*log.Entry + logLevels []log.Level + delay time.Duration } // NewEmailNotifier is a factory method creating a new email notifier instance @@ -42,19 +42,21 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") delay, _ := flags.GetInt("notification-email-delay") subjecttag, _ := flags.GetString("notification-email-subjecttag") + subjecthostname, _ := flags.GetString("notification-email-subjecthostname") n := &emailTypeNotifier{ - entries: []*log.Entry{}, - From: from, - To: to, - Server: server, - User: user, - Password: password, - Port: port, - tlsSkipVerify: tlsSkipVerify, - logLevels: acceptedLogLevels, - delay: time.Duration(delay) * time.Second, - SubjectTag: subjecttag, + entries: []*log.Entry{}, + From: from, + To: to, + Server: server, + User: user, + Password: password, + Port: port, + tlsSkipVerify: tlsSkipVerify, + logLevels: acceptedLogLevels, + delay: time.Duration(delay) * time.Second, + SubjectTag: subjecttag, + SubjectHostname: subjecthostname, } return n @@ -88,7 +90,7 @@ func (e *emailTypeNotifier) GetURL() (string, error) { } func (e *emailTypeNotifier) getSubject() string { - subject := GetTitle() + subject := GetTitle(e.SubjectHostname) if e.SubjectTag != "" { subject = e.SubjectTag + " " + subject diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index 7a6009bd3..b9614c9f9 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -77,7 +77,7 @@ func (n *gotifyTypeNotifier) GetURL() (string, error) { Host: apiURL.Host, Path: apiURL.Path, DisableTLS: apiURL.Scheme == "http", - Title: GetTitle(), + Title: GetTitle(""), Token: n.gotifyAppToken, } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index 6c4722943..6ae0ddf91 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -54,7 +54,7 @@ func (n *msTeamsTypeNotifier) GetURL() (string, error) { } config.Color = ColorHex - config.Title = GetTitle() + config.Title = GetTitle("") return config.GetURL().String(), nil } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index c4e962fa2..5eba84fc0 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -139,11 +139,15 @@ func (n *Notifier) Close() { } // GetTitle returns a common notification title with hostname appended -func GetTitle() (title string) { +func GetTitle(customHostname string) (title string) { title = "Watchtower updates" - if hostname, err := os.Hostname(); err == nil { - title += " on " + hostname + if customHostname != "" { + title += " on " + customHostname + } else { + if hostname, err := os.Hostname(); err == nil { + title += " on " + hostname + } } return diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index ba6657ad6..704830102 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -46,7 +46,7 @@ var _ = Describe("notifications", func() { channel := "123456789" token := "abvsihdbau" color := notifications.ColorInt - title := url.QueryEscape(notifications.GetTitle()) + title := url.QueryEscape(notifications.GetTitle("")) expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&splitlines=Yes&title=%s&username=watchtower", token, channel, color, title) buildArgs := func(url string) []string { return []string{ @@ -75,7 +75,7 @@ var _ = Describe("notifications", func() { tokenB := "bbb" tokenC := "ccc" color := url.QueryEscape(notifications.ColorHex) - title := url.QueryEscape(notifications.GetTitle()) + title := url.QueryEscape(notifications.GetTitle("")) hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) expectedOutput := fmt.Sprintf("slack://%s@%s/%s/%s?color=%s&title=%s", username, tokenA, tokenB, tokenC, color, title) @@ -99,7 +99,7 @@ var _ = Describe("notifications", func() { It("should return the expected URL", func() { token := "aaa" host := "shoutrrr.local" - title := url.QueryEscape(notifications.GetTitle()) + title := url.QueryEscape(notifications.GetTitle("")) expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title) @@ -125,7 +125,7 @@ var _ = Describe("notifications", func() { tokenB := "33333333012222222222333333333344" tokenC := "44444444-4444-4444-8444-cccccccccccc" color := url.QueryEscape(notifications.ColorHex) - title := url.QueryEscape(notifications.GetTitle()) + title := url.QueryEscape(notifications.GetTitle("")) hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC) expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title) diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index b3df119d4..65fc1eb11 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -57,7 +57,7 @@ func (s *slackTypeNotifier) GetURL() (string, error) { Channel: parts[len(parts)-3], Token: parts[len(parts)-2], Color: ColorInt, - Title: GetTitle(), + Title: GetTitle(""), SplitLines: true, Username: s.Username, } @@ -71,7 +71,7 @@ func (s *slackTypeNotifier) GetURL() (string, error) { BotName: s.Username, Token: tokens, Color: ColorHex, - Title: GetTitle(), + Title: GetTitle(""), } return conf.GetURL().String(), nil From b3bbc3ca0c676c04e700aff5e337cd5ab005b48a Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Fri, 18 Jun 2021 21:44:19 +1000 Subject: [PATCH 2/3] feat: allow custom hostname for all notifiers --- internal/flags/flags.go | 12 ++++---- pkg/notifications/email.go | 48 ++++++++++++++---------------- pkg/notifications/gotify.go | 4 +-- pkg/notifications/msteams.go | 4 +-- pkg/notifications/notifier.go | 18 ++++++----- pkg/notifications/notifier_test.go | 20 +++++++++---- pkg/notifications/slack.go | 6 ++-- pkg/types/convertible_notifier.go | 4 ++- 8 files changed, 64 insertions(+), 52 deletions(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 964c6b40d..9df42c1c6 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -185,6 +185,12 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") + flags.StringP( + "notifications-hostname", + "", + viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), + "Custom hostname for notification titles") + flags.StringP( "notification-email-from", "", @@ -240,12 +246,6 @@ Should only be used for testing.`) viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), "Subject prefix tag for notifications via mail") - flags.StringP( - "notification-email-subjecthostname", - "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTHOSTNAME"), - "Subject custom hostname for notifications via mail") - flags.StringP( "notification-slack-hook-url", "", diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 5b954db4f..e26ca97d4 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -15,14 +15,14 @@ const ( ) type emailTypeNotifier struct { - url string - From, To string - Server, User, Password, SubjectTag, SubjectHostname string - Port int - tlsSkipVerify bool - entries []*log.Entry - logLevels []log.Level - delay time.Duration + url string + From, To string + Server, User, Password, SubjectTag string + Port int + tlsSkipVerify bool + entries []*log.Entry + logLevels []log.Level + delay time.Duration } // NewEmailNotifier is a factory method creating a new email notifier instance @@ -42,34 +42,32 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") delay, _ := flags.GetInt("notification-email-delay") subjecttag, _ := flags.GetString("notification-email-subjecttag") - subjecthostname, _ := flags.GetString("notification-email-subjecthostname") n := &emailTypeNotifier{ - entries: []*log.Entry{}, - From: from, - To: to, - Server: server, - User: user, - Password: password, - Port: port, - tlsSkipVerify: tlsSkipVerify, - logLevels: acceptedLogLevels, - delay: time.Duration(delay) * time.Second, - SubjectTag: subjecttag, - SubjectHostname: subjecthostname, + entries: []*log.Entry{}, + From: from, + To: to, + Server: server, + User: user, + Password: password, + Port: port, + tlsSkipVerify: tlsSkipVerify, + logLevels: acceptedLogLevels, + delay: time.Duration(delay) * time.Second, + SubjectTag: subjecttag, } return n } -func (e *emailTypeNotifier) GetURL() (string, error) { +func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) { conf := &shoutrrrSmtp.Config{ FromAddress: e.From, FromName: "Watchtower", ToAddresses: []string{e.To}, Port: uint16(e.Port), Host: e.Server, - Subject: e.getSubject(), + Subject: e.getSubject(c), Username: e.User, Password: e.Password, UseStartTLS: !e.tlsSkipVerify, @@ -89,8 +87,8 @@ func (e *emailTypeNotifier) GetURL() (string, error) { return conf.GetURL().String(), nil } -func (e *emailTypeNotifier) getSubject() string { - subject := GetTitle(e.SubjectHostname) +func (e *emailTypeNotifier) getSubject(c *cobra.Command) string { + subject := GetTitle(c) if e.SubjectTag != "" { subject = e.SubjectTag + " " + subject diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index b9614c9f9..85f59b1c0 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -67,7 +67,7 @@ func getGotifyURL(flags *pflag.FlagSet) string { return gotifyURL } -func (n *gotifyTypeNotifier) GetURL() (string, error) { +func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) { apiURL, err := url.Parse(n.gotifyURL) if err != nil { return "", err @@ -77,7 +77,7 @@ func (n *gotifyTypeNotifier) GetURL() (string, error) { Host: apiURL.Host, Path: apiURL.Path, DisableTLS: apiURL.Scheme == "http", - Title: GetTitle(""), + Title: GetTitle(c), Token: n.gotifyAppToken, } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index 6ae0ddf91..282ce05ba 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -42,7 +42,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con return n } -func (n *msTeamsTypeNotifier) GetURL() (string, error) { +func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) { webhookURL, err := url.Parse(n.webHookURL) if err != nil { return "", err @@ -54,7 +54,7 @@ func (n *msTeamsTypeNotifier) GetURL() (string, error) { } config.Color = ColorHex - config.Title = GetTitle("") + config.Title = GetTitle(c) return config.GetURL().String(), nil } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 5eba84fc0..358c5f33e 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -98,7 +98,7 @@ func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level, continue } - shoutrrrURL, err := legacyNotifier.GetURL() + shoutrrrURL, err := legacyNotifier.GetURL(cmd) if err != nil { log.Fatal("failed to create notification config:", err) } @@ -139,15 +139,17 @@ func (n *Notifier) Close() { } // GetTitle returns a common notification title with hostname appended -func GetTitle(customHostname string) (title string) { +func GetTitle(c *cobra.Command) (title string) { title = "Watchtower updates" - if customHostname != "" { - title += " on " + customHostname - } else { - if hostname, err := os.Hostname(); err == nil { - title += " on " + hostname - } + f := c.PersistentFlags() + + hostname, _ := f.GetString("notifications-hostname") + + if hostname != "" { + title += " on " + hostname + } else if hostname, err := os.Hostname(); err == nil { + title += " on " + hostname } return diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 704830102..f95ecbc2e 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -43,10 +43,13 @@ var _ = Describe("notifications", func() { builderFn := notifications.NewSlackNotifier When("passing a discord url to the slack notifier", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + channel := "123456789" token := "abvsihdbau" color := notifications.ColorInt - title := url.QueryEscape(notifications.GetTitle("")) + title := url.QueryEscape(notifications.GetTitle(command)) expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&splitlines=Yes&title=%s&username=watchtower", token, channel, color, title) buildArgs := func(url string) []string { return []string{ @@ -69,13 +72,15 @@ var _ = Describe("notifications", func() { When("converting a slack service config into a shoutrrr url", func() { It("should return the expected URL", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) username := "containrrrbot" tokenA := "aaa" tokenB := "bbb" tokenC := "ccc" color := url.QueryEscape(notifications.ColorHex) - title := url.QueryEscape(notifications.GetTitle("")) + title := url.QueryEscape(notifications.GetTitle(command)) hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) expectedOutput := fmt.Sprintf("slack://%s@%s/%s/%s?color=%s&title=%s", username, tokenA, tokenB, tokenC, color, title) @@ -97,9 +102,12 @@ var _ = Describe("notifications", func() { builderFn := notifications.NewGotifyNotifier It("should return the expected URL", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + token := "aaa" host := "shoutrrr.local" - title := url.QueryEscape(notifications.GetTitle("")) + title := url.QueryEscape(notifications.GetTitle(command)) expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title) @@ -120,12 +128,14 @@ var _ = Describe("notifications", func() { builderFn := notifications.NewMsTeamsNotifier It("should return the expected URL", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) tokenA := "11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc" tokenB := "33333333012222222222333333333344" tokenC := "44444444-4444-4444-8444-cccccccccccc" color := url.QueryEscape(notifications.ColorHex) - title := url.QueryEscape(notifications.GetTitle("")) + title := url.QueryEscape(notifications.GetTitle(command)) hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC) expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title) @@ -215,7 +225,7 @@ func testURL(builder builderFn, args []string, expectedURL string) { Expect(err).NotTo(HaveOccurred()) notifier := builder(command, []log.Level{}) - actualURL, err := notifier.GetURL() + actualURL, err := notifier.GetURL(command) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 65fc1eb11..63cb44ce2 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -46,7 +46,7 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert return n } -func (s *slackTypeNotifier) GetURL() (string, error) { +func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) { trimmedURL := strings.TrimRight(s.HookURL, "/") trimmedURL = strings.TrimLeft(trimmedURL, "https://") parts := strings.Split(trimmedURL, "/") @@ -57,7 +57,7 @@ func (s *slackTypeNotifier) GetURL() (string, error) { Channel: parts[len(parts)-3], Token: parts[len(parts)-2], Color: ColorInt, - Title: GetTitle(""), + Title: GetTitle(c), SplitLines: true, Username: s.Username, } @@ -71,7 +71,7 @@ func (s *slackTypeNotifier) GetURL() (string, error) { BotName: s.Username, Token: tokens, Color: ColorHex, - Title: GetTitle(""), + Title: GetTitle(c), } return conf.GetURL().String(), nil diff --git a/pkg/types/convertible_notifier.go b/pkg/types/convertible_notifier.go index 2614d1213..87f8659cb 100644 --- a/pkg/types/convertible_notifier.go +++ b/pkg/types/convertible_notifier.go @@ -1,6 +1,8 @@ package types +import "github.com/spf13/cobra" + // ConvertibleNotifier is a notifier capable of creating a shoutrrr URL type ConvertibleNotifier interface { - GetURL() (string, error) + GetURL(c *cobra.Command) (string, error) } From d1d6aa850fd681768751ba6d2db8d6836d71de1c Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Fri, 18 Jun 2021 22:02:26 +1000 Subject: [PATCH 3/3] docs: adjust notification hostname flag --- docs/notifications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notifications.md b/docs/notifications.md index 683063b9c..f78386669 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -25,6 +25,7 @@ comma-separated list of values to the `--notifications` option ## Settings - `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info`, `debug` or `trace`. +- `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname. - Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument. ## Available services @@ -42,7 +43,6 @@ To receive notifications by email, the following command-line options, or their - `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used. - `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds. - `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. -- `--notification-email-subjecthostname` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTHOSTNAME`): Custom hostname specified in subject. Useful to override the hostname. Example: