From a5c60a9fe6acd59098be9ba5ed3f477874962c8b Mon Sep 17 00:00:00 2001 From: lazou Date: Wed, 9 Mar 2022 11:03:06 +0100 Subject: [PATCH] feat(notifications): add general notification delay (#1246) --- docs/notifications.md | 1 + internal/flags/flags.go | 6 +++ pkg/notifications/notifier.go | 19 +++++++- pkg/notifications/notifier_test.go | 77 ++++++++++++++++++++++++++---- 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/docs/notifications.md b/docs/notifications.md index faa4e4a..1a99269 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -26,6 +26,7 @@ comma-separated list of values to the `--notifications` option - `--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. +- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATION_DELAY`): Delay before sending notifications expressed in seconds. - 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 diff --git a/internal/flags/flags.go b/internal/flags/flags.go index a02dbd7..003ec30 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -184,6 +184,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.IntP( + "notifications-delay", + "", + viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"), + "Delay before sending notifications, expressed in seconds") + flags.StringP( "notifications-hostname", "", diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 61861fb..cf03b50 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -45,7 +45,7 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string log.WithError(err).Fatal("could not read notifications argument") } - delay := time.Duration(0) + legacyDelay := time.Duration(0) for _, t := range types { @@ -76,14 +76,29 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string urls = append(urls, shoutrrrURL) if delayNotifier, ok := legacyNotifier.(ty.DelayNotifier); ok { - delay = delayNotifier.GetDelay() + legacyDelay = delayNotifier.GetDelay() } log.WithField("URL", shoutrrrURL).Trace("created Shoutrrr URL from legacy notifier") } + + delay := GetDelay(cmd, legacyDelay) return urls, delay } +// GetDelay returns the legacy delay if defined, otherwise the delay as set by args is returned +func GetDelay(c *cobra.Command, legacyDelay time.Duration) time.Duration { + if legacyDelay > 0 { + return legacyDelay + } + + delay, _ := c.PersistentFlags().GetInt("notifications-delay") + if delay > 0 { + return time.Duration(delay) * time.Second + } + return time.Duration(0) +} + // GetTitle returns a common notification title with hostname appended func GetTitle(hostname string) string { title := "Watchtower updates" diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 44b4dad..4cb35a6 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "os" + "time" "github.com/containrrr/watchtower/cmd" "github.com/containrrr/watchtower/internal/flags" @@ -49,6 +50,51 @@ var _ = Describe("notifications", func() { Expect(title).To(Equal("Watchtower updates")) }) }) + When("no delay is defined", func() { + It("should use the default delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + delay := notifications.GetDelay(command, time.Duration(0)) + Expect(delay).To(Equal(time.Duration(0))) + }) + }) + When("delay is defined", func() { + It("should use the specified delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + err := command.ParseFlags([]string{ + "--notifications-delay", + "5", + }) + Expect(err).NotTo(HaveOccurred()) + delay := notifications.GetDelay(command, time.Duration(0)) + Expect(delay).To(Equal(time.Duration(5) * time.Second)) + }) + }) + When("legacy delay is defined", func() { + It("should use the specified legacy delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + delay := notifications.GetDelay(command, time.Duration(5)*time.Second) + Expect(delay).To(Equal(time.Duration(5) * time.Second)) + }) + }) + When("legacy delay and delay is defined", func() { + It("should use the specified legacy delay and ignore the specified delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + err := command.ParseFlags([]string{ + "--notifications-delay", + "0", + }) + Expect(err).NotTo(HaveOccurred()) + delay := notifications.GetDelay(command, time.Duration(7)*time.Second) + Expect(delay).To(Equal(time.Duration(7) * time.Second)) + }) + }) }) Describe("the slack notifier", func() { // builderFn := notifications.NewSlackNotifier @@ -74,11 +120,11 @@ var _ = Describe("notifications", func() { It("should return a discord url when using a hook url with the domain discord.com", func() { hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token) - testURL(buildArgs(hookURL), expected) + testURL(buildArgs(hookURL), expected, time.Duration(0)) }) It("should return a discord url when using a hook url with the domain discordapp.com", func() { hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discordapp.com", channel, token) - testURL(buildArgs(hookURL), expected) + testURL(buildArgs(hookURL), expected, time.Duration(0)) }) }) When("converting a slack service config into a shoutrrr url", func() { @@ -99,6 +145,7 @@ var _ = Describe("notifications", func() { hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s&title=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL), title) + expectedDelay := time.Duration(7) * time.Second args := []string{ "--notifications", @@ -109,9 +156,11 @@ var _ = Describe("notifications", func() { username, "--notification-slack-icon-url", iconURL, + "--notifications-delay", + fmt.Sprint(expectedDelay.Seconds()), } - testURL(args, expectedOutput) + testURL(args, expectedOutput, expectedDelay) }) }) @@ -131,7 +180,7 @@ var _ = Describe("notifications", func() { iconEmoji, } - testURL(args, expectedOutput) + testURL(args, expectedOutput, time.Duration(0)) }) }) }) @@ -159,7 +208,7 @@ var _ = Describe("notifications", func() { token, } - testURL(args, expectedOutput) + testURL(args, expectedOutput, time.Duration(0)) }) }) }) @@ -187,7 +236,7 @@ var _ = Describe("notifications", func() { hookURL, } - testURL(args, expectedOutput) + testURL(args, expectedOutput, time.Duration(0)) }) }) }) @@ -197,6 +246,8 @@ var _ = Describe("notifications", func() { It("should set the from address in the URL", func() { fromAddress := "lala@example.com" expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, "mail@example.com", "Plain") + expectedDelay := time.Duration(7) * time.Second + args := []string{ "--notifications", "email", @@ -210,8 +261,10 @@ var _ = Describe("notifications", func() { "secret-password", "--notification-email-server", "mail.containrrr.dev", + "--notifications-delay", + fmt.Sprint(expectedDelay.Seconds()), } - testURL(args, expectedOutput) + testURL(args, expectedOutput, expectedDelay) }) It("should return the expected URL", func() { @@ -219,6 +272,7 @@ var _ = Describe("notifications", func() { fromAddress := "sender@example.com" toAddress := "receiver@example.com" expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, toAddress, "Plain") + expectedDelay := time.Duration(7) * time.Second args := []string{ "--notifications", @@ -233,9 +287,11 @@ var _ = Describe("notifications", func() { "secret-password", "--notification-email-server", "mail.containrrr.dev", + "--notification-email-delay", + fmt.Sprint(expectedDelay.Seconds()), } - testURL(args, expectedOutput) + testURL(args, expectedOutput, expectedDelay) }) }) }) @@ -257,7 +313,7 @@ func buildExpectedURL(username string, password string, host string, port int, f url.QueryEscape(to)) } -func testURL(args []string, expectedURL string) { +func testURL(args []string, expectedURL string, expectedDelay time.Duration) { defer GinkgoRecover() command := cmd.NewRootCommand() @@ -268,9 +324,10 @@ func testURL(args []string, expectedURL string) { hostname := notifications.GetHostname(command) title := notifications.GetTitle(hostname) - urls, _ := notifications.AppendLegacyUrls([]string{}, command, title) + urls, delay := notifications.AppendLegacyUrls([]string{}, command, title) Expect(err).NotTo(HaveOccurred()) Expect(urls).To(ContainElement(expectedURL)) + Expect(delay).To(Equal(expectedDelay)) }