From 494859c122fb0ed775b52ecc55d0f39963ed60fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20ma=CC=8Ase=CC=81n?= Date: Fri, 11 Feb 2022 10:34:08 +0100 Subject: [PATCH] fix(notifications): title customization --- internal/flags/flags.go | 10 +++++ pkg/notifications/notifier.go | 60 +++++++++++++++++++------- pkg/notifications/notifier_test.go | 68 ++++++++++++++++++++++++------ pkg/notifications/shoutrrr.go | 27 +++++++----- pkg/notifications/shoutrrr_test.go | 13 +++--- 5 files changed, 132 insertions(+), 46 deletions(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index a02dbd742..c4b9ba1c8 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -320,6 +320,16 @@ Should only be used for testing.`) viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"), "Use the session report as the notification template data") + flags.StringP( + "notification-title-tag", + "", + viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"), + "Title prefix tag for notifications") + + flags.Bool("notification-skip-title", + viper.GetBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"), + "Do not pass the title param to notifications") + flags.String( "warn-on-head-failure", viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 61861fbbd..75a457f21 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -2,6 +2,7 @@ package notifications import ( "os" + "strings" "time" ty "github.com/containrrr/watchtower/pkg/types" @@ -10,6 +11,11 @@ import ( "github.com/spf13/cobra" ) +type TitleProvider struct { + Tag string + Hostname string +} + // NewNotifier creates and returns a new Notifier, using global configuration. func NewNotifier(c *cobra.Command) ty.Notifier { f := c.PersistentFlags() @@ -30,10 +36,10 @@ func NewNotifier(c *cobra.Command) ty.Notifier { tplString, _ := f.GetString("notification-template") urls, _ := f.GetStringArray("notification-url") - hostname := GetHostname(c) - urls, delay := AppendLegacyUrls(urls, c, GetTitle(hostname)) + data := GetTemplateData(c) + urls, delay := AppendLegacyUrls(urls, c, data.Title) - return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, hostname, delay, urls...) + return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, data, delay, urls...) } // AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags @@ -84,28 +90,50 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string return urls, delay } -// GetTitle returns a common notification title with hostname appended -func GetTitle(hostname string) string { - title := "Watchtower updates" +// GetTitle formats the title based on the passed hostname and tag +func GetTitle(hostname string, tag string) string { + tb := strings.Builder{} + + if tag != "" { + tb.WriteRune('[') + tb.WriteString(tag) + tb.WriteRune(']') + tb.WriteRune(' ') + } + + tb.WriteString("Watchtower updates") + if hostname != "" { - title += " on " + hostname + tb.WriteString(" on ") + tb.WriteString(hostname) } - return title -} -// GetHostname returns the hostname as set by args or resolved from OS -func GetHostname(c *cobra.Command) string { + return tb.String() +} +// GetTemplateData populates the static notification data from flags and environment +func GetTemplateData(c *cobra.Command) StaticData { f := c.PersistentFlags() + hostname, _ := f.GetString("notifications-hostname") + if hostname == "" { + hostname, _ = os.Hostname() + } - if hostname != "" { - return hostname - } else if hostname, err := os.Hostname(); err == nil { - return hostname + title := "" + if skip, _ := f.GetBool("notification-skip-title"); !skip { + tag, _ := f.GetString("notification-title-tag") + if tag == "" { + // For legacy email support + tag, _ = f.GetString("notification-email-subjecttag") + } + title = GetTitle(hostname, tag) } - return "" + return StaticData{ + Host: hostname, + Title: title, + } } // ColorHex is the default notification color used for services that support it (formatted as a CSS hex string) diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 44b4dad81..179857180 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -38,17 +38,58 @@ var _ = Describe("notifications", func() { "test.host", }) Expect(err).NotTo(HaveOccurred()) - hostname := notifications.GetHostname(command) - title := notifications.GetTitle(hostname) + data := notifications.GetTemplateData(command) + title := data.Title Expect(title).To(Equal("Watchtower updates on test.host")) }) }) When("no hostname can be resolved", func() { It("should use the default simple title", func() { - title := notifications.GetTitle("") + title := notifications.GetTitle("", "") Expect(title).To(Equal("Watchtower updates")) }) }) + When("title tag is set", func() { + It("should use the prefix in the title", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + Expect(command.ParseFlags([]string{ + "--notification-title-tag", + "PREFIX", + })).To(Succeed()) + + data := notifications.GetTemplateData(command) + Expect(data.Title).To(HavePrefix("[PREFIX]")) + }) + }) + When("legacy email tag is set", func() { + It("should use the prefix in the title", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + Expect(command.ParseFlags([]string{ + "--notification-email-subjecttag", + "PREFIX", + })).To(Succeed()) + + data := notifications.GetTemplateData(command) + Expect(data.Title).To(HavePrefix("[PREFIX]")) + }) + }) + When("the skip title flag is set", func() { + It("should return an empty title", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + Expect(command.ParseFlags([]string{ + "--notification-skip-title", + })).To(Succeed()) + + data := notifications.GetTemplateData(command) + Expect(data.Title).To(BeEmpty()) + }) + }) }) Describe("the slack notifier", func() { // builderFn := notifications.NewSlackNotifier @@ -60,8 +101,8 @@ var _ = Describe("notifications", func() { channel := "123456789" token := "abvsihdbau" color := notifications.ColorInt - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) + data := notifications.GetTemplateData(command) + title := url.QueryEscape(data.Title) expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&title=%s&username=watchtower", token, channel, color, title) buildArgs := func(url string) []string { return []string{ @@ -89,8 +130,8 @@ var _ = Describe("notifications", func() { tokenB := "BBBBBBBBB" tokenC := "123456789123456789123456" color := url.QueryEscape(notifications.ColorHex) - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) + data := notifications.GetTemplateData(command) + title := url.QueryEscape(data.Title) iconURL := "https://containrrr.dev/watchtower-sq180.png" iconEmoji := "whale" @@ -145,8 +186,8 @@ var _ = Describe("notifications", func() { token := "aaa" host := "shoutrrr.local" - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) + data := notifications.GetTemplateData(command) + title := url.QueryEscape(data.Title) expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title) @@ -174,8 +215,8 @@ var _ = Describe("notifications", func() { tokenB := "33333333012222222222333333333344" tokenC := "44444444-4444-4444-8444-cccccccccccc" color := url.QueryEscape(notifications.ColorHex) - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) + data := notifications.GetTemplateData(command) + title := url.QueryEscape(data.Title) 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) @@ -266,9 +307,8 @@ func testURL(args []string, expectedURL string) { err := command.ParseFlags(args) Expect(err).NotTo(HaveOccurred()) - hostname := notifications.GetHostname(command) - title := notifications.GetTitle(hostname) - urls, _ := notifications.AppendLegacyUrls([]string{}, command, title) + data := notifications.GetTemplateData(command) + urls, _ := notifications.AppendLegacyUrls([]string{}, command, data.Title) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index bc9499ea0..19f33c6d6 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -58,7 +58,7 @@ type shoutrrrTypeNotifier struct { done chan bool legacyTemplate bool params *types.Params - hostname string + data StaticData } // GetScheme returns the scheme part of a Shoutrrr URL @@ -79,11 +79,9 @@ func (n *shoutrrrTypeNotifier) GetNames() []string { return names } -func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, hostname string, delay time.Duration, urls ...string) t.Notifier { +func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, data StaticData, delay time.Duration, urls ...string) t.Notifier { - notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy) - notifier.hostname = hostname - notifier.params = &types.Params{"title": GetTitle(hostname)} + notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy, data) log.AddHook(notifier) // Do the sending in a separate goroutine so we don't block the main process. @@ -92,7 +90,7 @@ func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy return notifier } -func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool) *shoutrrrTypeNotifier { +func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool, data StaticData) *shoutrrrTypeNotifier { tpl, err := getShoutrrrTemplate(tplString, legacy) if err != nil { log.Errorf("Could not use configured notification template: %s. Using default template", err) @@ -112,6 +110,10 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy logLevels: levels, template: tpl, legacyTemplate: legacy, + data: data, + params: &types.Params{ + "title": data.Title, + }, } } @@ -149,9 +151,7 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) { } func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) { - title, _ := n.params.Title() - host := n.hostname - msg, err := n.buildMessage(Data{entries, report, title, host}) + msg, err := n.buildMessage(Data{n.data, entries, report}) if msg == "" { // Log in go func in case we entered from Fire to avoid stalling @@ -240,10 +240,15 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, return } +// StaticData is the part of the notification template data model set upon initialization +type StaticData struct { + Title string + Host string +} + // Data is the notification template data model type Data struct { + StaticData Entries []*log.Entry Report t.Report - Title string - Host string } diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index dbdd9eba2..4b3d4ca17 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -49,11 +49,14 @@ var mockDataAllFresh = Data{ func mockDataFromStates(states ...s.State) Data { hostname := "Mock" + prefix := "" return Data{ Entries: legacyMockData.Entries, Report: mocks.CreateMockProgressReport(states...), - Title: GetTitle(hostname), - Host: hostname, + StaticData: StaticData{ + Title: GetTitle(hostname, prefix), + Host: hostname, + }, } } @@ -77,7 +80,7 @@ var _ = Describe("Shoutrrr", func() { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) - shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true) + shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{}) entries := []*logrus.Entry{ { @@ -233,7 +236,7 @@ Turns out everything is on fire When("batching notifications", func() { When("no messages are queued", func() { It("should not send any notification", func() { - shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://") + shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), "logger://") shoutrrr.StartNotification() shoutrrr.SendNotification(nil) Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`)) @@ -241,7 +244,7 @@ Turns out everything is on fire }) When("at least one message is queued", func() { It("should send a notification", func() { - shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://") + shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), "logger://") shoutrrr.StartNotification() logrus.Info("This log message is sponsored by ContainrrrVPN") shoutrrr.SendNotification(nil)