From aa02d8d31bb2339ad02c388ebae686da3568b000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Wed, 5 Jan 2022 12:08:47 +0100 Subject: [PATCH] feat(notifications): add title field to template data (#1125) --- pkg/notifications/email.go | 12 ++++------ pkg/notifications/gotify.go | 4 ++-- pkg/notifications/msteams.go | 7 +++--- pkg/notifications/notifier.go | 29 ++++++++++++++--------- pkg/notifications/notifier_test.go | 37 ++++++++++++++++++++++++++---- pkg/notifications/shoutrrr.go | 12 +++++++--- pkg/notifications/shoutrrr_test.go | 20 ++++++++++++++++ pkg/notifications/slack.go | 6 ++--- pkg/types/convertible_notifier.go | 2 +- 9 files changed, 94 insertions(+), 35 deletions(-) diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 2b1b85a..e162209 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -55,14 +55,14 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert return n } -func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) { +func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { conf := &shoutrrrSmtp.Config{ FromAddress: e.From, FromName: "Watchtower", ToAddresses: []string{e.To}, Port: uint16(e.Port), Host: e.Server, - Subject: e.getSubject(c), + Subject: e.getSubject(c, title), Username: e.User, Password: e.Password, UseStartTLS: !e.tlsSkipVerify, @@ -86,12 +86,10 @@ func (e *emailTypeNotifier) GetDelay() time.Duration { return e.delay } -func (e *emailTypeNotifier) getSubject(c *cobra.Command) string { - subject := GetTitle(c) - +func (e *emailTypeNotifier) getSubject(_ *cobra.Command, title string) string { if e.SubjectTag != "" { - subject = e.SubjectTag + " " + subject + return e.SubjectTag + " " + title } - return subject + return title } diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index 6f01000..a8c9ac4 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -62,7 +62,7 @@ func getGotifyURL(flags *pflag.FlagSet) string { return gotifyURL } -func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) { +func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { apiURL, err := url.Parse(n.gotifyURL) if err != nil { return "", err @@ -72,7 +72,7 @@ func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) { Host: apiURL.Host, Path: apiURL.Path, DisableTLS: apiURL.Scheme == "http", - Title: GetTitle(c), + Title: title, Token: n.gotifyAppToken, } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index b95a99e..be67d3b 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -1,11 +1,12 @@ package notifications import ( + "net/url" + shoutrrrTeams "github.com/containrrr/shoutrrr/pkg/services/teams" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "net/url" ) const ( @@ -37,7 +38,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con return n } -func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) { +func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { webhookURL, err := url.Parse(n.webHookURL) if err != nil { return "", err @@ -49,7 +50,7 @@ func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) { } config.Color = ColorHex - config.Title = GetTitle(c) + config.Title = title return config.GetURL().String(), nil } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 5bfa08c..61861fb 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -30,14 +30,14 @@ func NewNotifier(c *cobra.Command) ty.Notifier { tplString, _ := f.GetString("notification-template") urls, _ := f.GetStringArray("notification-url") - urls, delay := AppendLegacyUrls(urls, c) + hostname := GetHostname(c) + urls, delay := AppendLegacyUrls(urls, c, GetTitle(hostname)) - title := GetTitle(c) - return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, title, delay, urls...) + return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, hostname, delay, urls...) } // AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags -func AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duration) { +func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string, time.Duration) { // Parse types and create notifiers. types, err := cmd.Flags().GetStringSlice("notifications") @@ -69,7 +69,7 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duratio continue } - shoutrrrURL, err := legacyNotifier.GetURL(cmd) + shoutrrrURL, err := legacyNotifier.GetURL(cmd, title) if err != nil { log.Fatal("failed to create notification config: ", err) } @@ -85,20 +85,27 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duratio } // GetTitle returns a common notification title with hostname appended -func GetTitle(c *cobra.Command) (title string) { - title = "Watchtower updates" +func GetTitle(hostname string) string { + title := "Watchtower updates" + if hostname != "" { + title += " on " + hostname + } + return title +} - f := c.PersistentFlags() +// GetHostname returns the hostname as set by args or resolved from OS +func GetHostname(c *cobra.Command) string { + f := c.PersistentFlags() hostname, _ := f.GetString("notifications-hostname") if hostname != "" { - title += " on " + hostname + return hostname } else if hostname, err := os.Hostname(); err == nil { - title += " on " + hostname + return hostname } - return + return "" } // 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 05d6d56..44b4dad 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -28,6 +28,27 @@ var _ = Describe("notifications", func() { Expect(notif.GetNames()).To(BeEmpty()) }) + When("title is overriden in flag", func() { + It("should use the specified hostname in the title", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + err := command.ParseFlags([]string{ + "--notifications-hostname", + "test.host", + }) + Expect(err).NotTo(HaveOccurred()) + hostname := notifications.GetHostname(command) + title := notifications.GetTitle(hostname) + 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("") + Expect(title).To(Equal("Watchtower updates")) + }) + }) }) Describe("the slack notifier", func() { // builderFn := notifications.NewSlackNotifier @@ -39,7 +60,8 @@ var _ = Describe("notifications", func() { channel := "123456789" token := "abvsihdbau" color := notifications.ColorInt - title := url.QueryEscape(notifications.GetTitle(command)) + hostname := notifications.GetHostname(command) + title := url.QueryEscape(notifications.GetTitle(hostname)) 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{ @@ -67,7 +89,8 @@ var _ = Describe("notifications", func() { tokenB := "BBBBBBBBB" tokenC := "123456789123456789123456" color := url.QueryEscape(notifications.ColorHex) - title := url.QueryEscape(notifications.GetTitle(command)) + hostname := notifications.GetHostname(command) + title := url.QueryEscape(notifications.GetTitle(hostname)) iconURL := "https://containrrr.dev/watchtower-sq180.png" iconEmoji := "whale" @@ -122,7 +145,8 @@ var _ = Describe("notifications", func() { token := "aaa" host := "shoutrrr.local" - title := url.QueryEscape(notifications.GetTitle(command)) + hostname := notifications.GetHostname(command) + title := url.QueryEscape(notifications.GetTitle(hostname)) expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title) @@ -150,7 +174,8 @@ var _ = Describe("notifications", func() { tokenB := "33333333012222222222333333333344" tokenC := "44444444-4444-4444-8444-cccccccccccc" color := url.QueryEscape(notifications.ColorHex) - title := url.QueryEscape(notifications.GetTitle(command)) + hostname := notifications.GetHostname(command) + title := url.QueryEscape(notifications.GetTitle(hostname)) 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) @@ -241,7 +266,9 @@ func testURL(args []string, expectedURL string) { err := command.ParseFlags(args) Expect(err).NotTo(HaveOccurred()) - urls, _ := notifications.AppendLegacyUrls([]string{}, command) + hostname := notifications.GetHostname(command) + title := notifications.GetTitle(hostname) + urls, _ := notifications.AppendLegacyUrls([]string{}, command, title) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index e41ed92..bc9499e 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -58,6 +58,7 @@ type shoutrrrTypeNotifier struct { done chan bool legacyTemplate bool params *types.Params + hostname string } // GetScheme returns the scheme part of a Shoutrrr URL @@ -78,10 +79,11 @@ func (n *shoutrrrTypeNotifier) GetNames() []string { return names } -func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, title string, delay time.Duration, urls ...string) t.Notifier { +func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, hostname string, delay time.Duration, urls ...string) t.Notifier { notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy) - notifier.params = &types.Params{"title": title} + notifier.hostname = hostname + notifier.params = &types.Params{"title": GetTitle(hostname)} log.AddHook(notifier) // Do the sending in a separate goroutine so we don't block the main process. @@ -147,7 +149,9 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) { } func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) { - msg, err := n.buildMessage(Data{entries, report}) + title, _ := n.params.Title() + host := n.hostname + msg, err := n.buildMessage(Data{entries, report, title, host}) if msg == "" { // Log in go func in case we entered from Fire to avoid stalling @@ -240,4 +244,6 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, type Data struct { 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 6e67b6f..dbdd9eb 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -48,9 +48,12 @@ var mockDataAllFresh = Data{ } func mockDataFromStates(states ...s.State) Data { + hostname := "Mock" return Data{ Entries: legacyMockData.Entries, Report: mocks.CreateMockProgressReport(states...), + Title: GetTitle(hostname), + Host: hostname, } } @@ -177,6 +180,22 @@ var _ = Describe("Shoutrrr", func() { }) + When("using a template referencing Title", func() { + It("should contain the title in the output", func() { + expected := `Watchtower updates on Mock` + data := mockDataFromStates(s.UpdatedState) + Expect(getTemplatedResult(`{{ .Title }}`, false, data)).To(Equal(expected)) + }) + }) + + When("using a template referencing Host", func() { + It("should contain the hostname in the output", func() { + expected := `Mock` + data := mockDataFromStates(s.UpdatedState) + Expect(getTemplatedResult(`{{ .Host }}`, false, data)).To(Equal(expected)) + }) + }) + Describe("the default template", func() { When("all containers are fresh", func() { It("should return an empty string", func() { @@ -278,6 +297,7 @@ func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *b done: make(chan bool), Router: router, legacyTemplate: legacy, + params: &types.Params{}, } entry := &logrus.Entry{ diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index c5c73b2..faff944 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -41,7 +41,7 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert return n } -func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) { +func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { trimmedURL := strings.TrimRight(s.HookURL, "/") trimmedURL = strings.TrimLeft(trimmedURL, "https://") parts := strings.Split(trimmedURL, "/") @@ -52,7 +52,7 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) { WebhookID: parts[len(parts)-3], Token: parts[len(parts)-2], Color: ColorInt, - Title: GetTitle(c), + Title: title, SplitLines: true, Username: s.Username, } @@ -65,7 +65,7 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) { BotName: s.Username, Color: ColorHex, Channel: "webhook", - Title: GetTitle(c), + Title: title, } if s.IconURL != "" { diff --git a/pkg/types/convertible_notifier.go b/pkg/types/convertible_notifier.go index 90f0078..37d8872 100644 --- a/pkg/types/convertible_notifier.go +++ b/pkg/types/convertible_notifier.go @@ -7,7 +7,7 @@ import ( // ConvertibleNotifier is a notifier capable of creating a shoutrrr URL type ConvertibleNotifier interface { - GetURL(c *cobra.Command) (string, error) + GetURL(c *cobra.Command, title string) (string, error) } // DelayNotifier is a notifier that might need to be delayed before sending notifications