From cb555f539d9662b7e9232d1dc2bed5abd3da3a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Tue, 1 Nov 2022 00:00:00 +0100 Subject: [PATCH] preparations for soft deprecation of legacy notification args (#1377) Co-authored-by: Simon Aronsson --- cmd/notify-upgrade.go | 111 ++++++++++++ cmd/root.go | 2 + docs/notifications.md | 264 +++++++++++++++++++++++------ go.mod | 2 - go.sum | 4 - pkg/container/cgroup_id.go | 29 ++++ pkg/container/cgroup_id_test.go | 40 +++++ pkg/container/container.go | 1 + pkg/notifications/email.go | 31 +--- pkg/notifications/gotify.go | 9 +- pkg/notifications/msteams.go | 9 +- pkg/notifications/notifier.go | 25 +-- pkg/notifications/notifier_test.go | 32 +--- pkg/notifications/shoutrrr.go | 32 ++-- pkg/notifications/shoutrrr_test.go | 38 ++++- pkg/notifications/slack.go | 28 ++- pkg/types/convertible_notifier.go | 7 +- pkg/types/notifier.go | 2 + 18 files changed, 502 insertions(+), 164 deletions(-) create mode 100644 cmd/notify-upgrade.go create mode 100644 pkg/container/cgroup_id.go create mode 100644 pkg/container/cgroup_id_test.go diff --git a/cmd/notify-upgrade.go b/cmd/notify-upgrade.go new file mode 100644 index 0000000..9991ee6 --- /dev/null +++ b/cmd/notify-upgrade.go @@ -0,0 +1,111 @@ +// Package cmd contains the watchtower (sub-)commands +package cmd + +import ( + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/containrrr/watchtower/internal/flags" + "github.com/containrrr/watchtower/pkg/container" + "github.com/containrrr/watchtower/pkg/notifications" + "github.com/spf13/cobra" +) + +var notifyUpgradeCommand = NewNotifyUpgradeCommand() + +// NewNotifyUpgradeCommand creates the notify upgrade command for watchtower +func NewNotifyUpgradeCommand() *cobra.Command { + return &cobra.Command{ + Use: "notify-upgrade", + Short: "Upgrade legacy notification configuration to shoutrrr URLs", + Run: runNotifyUpgrade, + } +} + +func runNotifyUpgrade(cmd *cobra.Command, args []string) { + if err := runNotifyUpgradeE(cmd, args); err != nil { + logf("Notification upgrade failed: %v", err) + } +} + +func runNotifyUpgradeE(cmd *cobra.Command, _ []string) error { + f := cmd.Flags() + flags.ProcessFlagAliases(f) + + notifier = notifications.NewNotifier(cmd) + urls := notifier.GetURLs() + + logf("Found notification configurations for: %v", strings.Join(notifier.GetNames(), ", ")) + + outFile, err := os.CreateTemp("/", "watchtower-notif-urls-*") + if err != nil { + return fmt.Errorf("failed to create output file: %v", err) + } + logf("Writing notification URLs to %v", outFile.Name()) + logf("") + + sb := strings.Builder{} + sb.WriteString("WATCHTOWER_NOTIFICATION_URL=") + + for i, u := range urls { + if i != 0 { + sb.WriteRune(' ') + } + sb.WriteString(u) + } + + _, err = fmt.Fprint(outFile, sb.String()) + tryOrLog(err, "Failed to write to output file") + + tryOrLog(outFile.Sync(), "Failed to sync output file") + tryOrLog(outFile.Close(), "Failed to close output file") + + containerID := "" + cid, err := container.GetRunningContainerID() + tryOrLog(err, "Failed to get running container ID") + if cid != "" { + containerID = cid.ShortID() + } + logf("To get the environment file, use:") + logf("cp %v:%v ./watchtower-notifications.env", containerID, outFile.Name()) + logf("") + logf("Note: This file will be removed in 5 minutes or when this container is stopped!") + + signalChannel := make(chan os.Signal, 1) + time.AfterFunc(5*time.Minute, func() { + signalChannel <- syscall.SIGALRM + }) + + signal.Notify(signalChannel, os.Interrupt) + signal.Notify(signalChannel, syscall.SIGTERM) + + switch <-signalChannel { + case syscall.SIGALRM: + logf("Timed out!") + case os.Interrupt, syscall.SIGTERM: + logf("Stopping...") + default: + } + + if err := os.Remove(outFile.Name()); err != nil { + logf("Failed to remove file, it may still be present in the container image! Error: %v", err) + } else { + logf("Environment file has been removed.") + } + + return nil +} + +func tryOrLog(err error, message string) { + if err != nil { + logf("%v: %v\n", message, err) + } +} + +func logf(format string, v ...interface{}) { + fmt.Fprintln(os.Stderr, fmt.Sprintf(format, v...)) +} diff --git a/cmd/root.go b/cmd/root.go index f79b660..838465b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,6 +66,7 @@ func init() { // Execute the root func and exit in case of errors func Execute() { + rootCmd.AddCommand(notifyUpgradeCommand) if err := rootCmd.Execute(); err != nil { log.Fatal(err) } @@ -139,6 +140,7 @@ func PreRun(cmd *cobra.Command, _ []string) { }) notifier = notifications.NewNotifier(cmd) + notifier.AddLogHook() } // Run is the main execution flow of the command diff --git a/docs/notifications.md b/docs/notifications.md index a69b00b..3905abf 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -1,15 +1,7 @@ # Notifications Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging -system, [logrus](http://github.com/sirupsen/logrus). The types of notifications to send are set by passing a -comma-separated list of values to the `--notifications` option -(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values: - -- `email` to send notifications via e-mail -- `slack` to send notifications through a Slack webhook -- `msteams` to send notifications via MSTeams webhook -- `gotify` to send notifications via Gotify -- `shoutrrr` to send notifications via [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) +system, [logrus](http://github.com/sirupsen/logrus). !!! note "Using multiple notifications with environment variables" There is currently a bug in Viper (https://github.com/spf13/viper/issues/380), which prevents comma-separated slices to @@ -31,7 +23,221 @@ comma-separated list of values to the `--notifications` option - `notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers. - `notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together. -## Available services +## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications + +To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set: + +- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used. + + +Go to [containrrr.dev/shoutrrr/v0.6/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to +learn more about the different service URLs you can use. You can define multiple services by space separating the +URLs. (See example below) + +You can customize the message posted by setting a template. + +- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message. + +The template is a Go [template](https://golang.org/pkg/text/template/) that either format a list +of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry) or a `notification.Data` struct. + +Simple templates are used unless the `notification-report` flag is specified: + +- `--notification-report` (env. `WATCHTOWER_NOTIFICATION_REPORT`): Use the session report as the notification template data. + +## Simple templates + +The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also +outputs timestamp and log level. + +!!! tip "Custom date format" + If you want to adjust the date/time format it must show how the + [reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your + custom format. + i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc. + +Example: + +```bash +docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ + -e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \ + containrrr/watchtower +``` + +## Report templates + +The default template for report notifications are the following: +```go +{{- if .Report -}} + {{- with .Report -}} + {{- if ( or .Updated .Failed ) -}} +{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed + {{- range .Updated}} +- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} + {{- end -}} + {{- range .Fresh}} +- {{.Name}} ({{.ImageName}}): {{.State}} + {{- end -}} + {{- range .Skipped}} +- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- range .Failed}} +- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} +{{- end -}} +``` + +It will be used to send a summary of every session if there are any containers that were updated or which failed to update. + +!!! note "Skipping notifications" + Whenever the result of applying the template results in an empty string, no notifications will + be sent. This is by default used to limit the notifications to only be sent when there something noteworthy occurred. + + You can replace `{{- if ( or .Updated .Failed ) -}}` with any logic you want to decide when to send the notifications. + +Example using a custom report template that always sends a session report after each run: + +=== "docker run" + + ```bash + docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATION_REPORT="true" + -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ + -e WATCHTOWER_NOTIFICATION_TEMPLATE=" + {{- if .Report -}} + {{- with .Report -}} + {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed + {{- range .Updated}} + - {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} + {{- end -}} + {{- range .Fresh}} + - {{.Name}} ({{.ImageName}}): {{.State}} + {{- end -}} + {{- range .Skipped}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- range .Failed}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- end -}} + {{- else -}} + {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} + {{- end -}} + " \ + containrrr/watchtower + ``` + +=== "docker-compose" + + ``` yaml + version: "3" + services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env: + WATCHTOWER_NOTIFICATION_REPORT: "true" + WATCHTOWER_NOTIFICATION_URL: > + discord://token@channel + slack://watchtower@token-a/token-b/token-c + WATCHTOWER_NOTIFICATION_TEMPLATE: | + {{- if .Report -}} + {{- with .Report -}} + {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed + {{- range .Updated}} + - {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} + {{- end -}} + {{- range .Fresh}} + - {{.Name}} ({{.ImageName}}): {{.State}} + {{- end -}} + {{- range .Skipped}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- range .Failed}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- end -}} + {{- else -}} + {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} + {{- end -}} + ``` + +## Legacy notifications + +For backwards compatibility, the notifications can also be configured using legacy notification options. These will automatically be converted to shoutrrr URLs when used. +The types of notifications to send are set by passing a comma-separated list of values to the `--notifications` option +(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values: + +- `email` to send notifications via e-mail +- `slack` to send notifications through a Slack webhook +- `msteams` to send notifications via MSTeams webhook +- `gotify` to send notifications via Gotify + +### `notify-upgrade` +If watchtower is started with `notify-upgrade` as it's first argument, it will generate a .env file with your current legacy notification options converted to shoutrrr URLs. + +=== "docker run" + + ```bash + $ docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATIONS=slack \ + -e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL="https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy" \ + containrrr/watchtower \ + notify-upgrade + ``` + +=== "docker-compose.yml" + + ```yaml + version: "3" + services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env: + WATCHTOWER_NOTIFICATIONS: slack + WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy + command: notify-upgrade + ``` + + +You can then copy this file from the container (a message with the full command to do so will be logged) and use it with your current setup: + +=== "docker run" + + ```bash + $ docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --env-file watchtower-notifications.env \ + containrrr/watchtower + ``` + +=== "docker-compose.yml" + + ```yaml + version: "3" + services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env_file: + - watchtower-notifications.env + ``` ### Email @@ -177,41 +383,3 @@ docker run -d \ If you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`. -### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) - -To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set: - -- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used. - - -Go to [containrrr.dev/shoutrrr/v0.6/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to -learn more about the different service URLs you can use. You can define multiple services by space separating the -URLs. (See example below) - -You can customize the message posted by setting a template. - -- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message. - -The template is a Go [template](https://golang.org/pkg/text/template/) and that format a list -of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry). - -The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also -outputs timestamp and log level. - -!!! tip "Custom date format" - If you want to adjust the date/time format it must show how the - [reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your - custom format. - i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc. - -Example: - -```bash -docker run -d \ - --name watchtower \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e WATCHTOWER_NOTIFICATIONS=shoutrrr \ - -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ - -e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \ - containrrr/watchtower -``` diff --git a/go.mod b/go.mod index d73d2e0..4ae46c2 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/docker/distribution v2.8.1+incompatible github.com/docker/docker v20.10.20+incompatible github.com/docker/go-connections v0.4.0 - github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.22.1 github.com/prometheus/client_golang v1.13.0 @@ -36,7 +35,6 @@ require ( github.com/google/go-cmp v0.5.8 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index 602f82b..61a82c1 100644 --- a/go.sum +++ b/go.sum @@ -204,10 +204,6 @@ github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7P github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 h1:jKUP9TQ0c7X3w6+IPyMit07RE42MtTWNd77sN2cHngQ= -github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22/go.mod h1:u0Jo4f2dNlTJeeOywkM6bLwxq6gC3pZ9rEFHn3AhTdk= -github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 h1:+kBG/8rjCa6vxJZbUjAiE4MQmBEBYc8nLEb51frnvBY= -github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= diff --git a/pkg/container/cgroup_id.go b/pkg/container/cgroup_id.go new file mode 100644 index 0000000..1da1dfe --- /dev/null +++ b/pkg/container/cgroup_id.go @@ -0,0 +1,29 @@ +package container + +import ( + "fmt" + "os" + "regexp" + + "github.com/containrrr/watchtower/pkg/types" +) + +var dockerContainerPattern = regexp.MustCompile(`[0-9]+:.*:/docker/([a-f|0-9]{64})`) + +// GetRunningContainerID tries to resolve the current container ID from the current process cgroup information +func GetRunningContainerID() (cid types.ContainerID, err error) { + file, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", os.Getpid())) + if err != nil { + return + } + + return getRunningContainerIDFromString(string(file)), nil +} + +func getRunningContainerIDFromString(s string) types.ContainerID { + matches := dockerContainerPattern.FindStringSubmatch(s) + if len(matches) < 2 { + return "" + } + return types.ContainerID(matches[1]) +} diff --git a/pkg/container/cgroup_id_test.go b/pkg/container/cgroup_id_test.go new file mode 100644 index 0000000..5f694e3 --- /dev/null +++ b/pkg/container/cgroup_id_test.go @@ -0,0 +1,40 @@ +package container + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetRunningContainerID", func() { + When("a matching container ID is found", func() { + It("should return that container ID", func() { + cid := getRunningContainerIDFromString(` +15:name=systemd:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +14:misc:/ +13:rdma:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +12:pids:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +11:hugetlb:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +10:net_prio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +9:perf_event:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +8:net_cls:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +7:freezer:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +6:devices:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +5:blkio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +4:cpuacct:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +3:cpu:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +2:cpuset:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +1:memory:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +0::/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 + `) + Expect(cid).To(BeEquivalentTo(`991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377`)) + }) + }) + When("no matching container ID could be found", func() { + It("should return that container ID", func() { + cid := getRunningContainerIDFromString(`14:misc:/`) + Expect(cid).To(BeEmpty()) + }) + }) +}) + +// diff --git a/pkg/container/container.go b/pkg/container/container.go index 82ae205..0bbea16 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -1,3 +1,4 @@ +// Package container contains code related to dealing with docker containers package container import ( diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 3ebb4c0..b6883a2 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -15,17 +15,16 @@ const ( ) type emailTypeNotifier struct { - From, To string - Server, User, Password, SubjectTag string - Port int - tlsSkipVerify bool - entries []*log.Entry - logLevels []log.Level - delay time.Duration + From, To string + Server, User, Password string + Port int + tlsSkipVerify bool + entries []*log.Entry + delay time.Duration } -func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { - flags := c.PersistentFlags() +func newEmailNotifier(c *cobra.Command) t.ConvertibleNotifier { + flags := c.Flags() from, _ := flags.GetString("notification-email-from") to, _ := flags.GetString("notification-email-to") @@ -35,7 +34,6 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert port, _ := flags.GetInt("notification-email-server-port") tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") delay, _ := flags.GetInt("notification-email-delay") - subjecttag, _ := flags.GetString("notification-email-subjecttag") n := &emailTypeNotifier{ entries: []*log.Entry{}, @@ -46,22 +44,19 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert Password: password, Port: port, tlsSkipVerify: tlsSkipVerify, - logLevels: acceptedLogLevels, delay: time.Duration(delay) * time.Second, - SubjectTag: subjecttag, } return n } -func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (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(c, title), Username: e.User, Password: e.Password, UseStartTLS: !e.tlsSkipVerify, @@ -84,11 +79,3 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro func (e *emailTypeNotifier) GetDelay() time.Duration { return e.delay } - -func (e *emailTypeNotifier) getSubject(_ *cobra.Command, title string) string { - if e.SubjectTag != "" { - return e.SubjectTag + " " + title - } - - return title -} diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index a8c9ac4..c36eb4b 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -19,11 +19,10 @@ type gotifyTypeNotifier struct { gotifyURL string gotifyAppToken string gotifyInsecureSkipVerify bool - logLevels []log.Level } -func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier { - flags := c.PersistentFlags() +func newGotifyNotifier(c *cobra.Command) t.ConvertibleNotifier { + flags := c.Flags() apiURL := getGotifyURL(flags) token := getGotifyToken(flags) @@ -34,7 +33,6 @@ func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifi gotifyURL: apiURL, gotifyAppToken: token, gotifyInsecureSkipVerify: skipVerify, - logLevels: levels, } return n @@ -62,7 +60,7 @@ func getGotifyURL(flags *pflag.FlagSet) string { return gotifyURL } -func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { +func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) { apiURL, err := url.Parse(n.gotifyURL) if err != nil { return "", err @@ -72,7 +70,6 @@ func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, err Host: apiURL.Host, Path: apiURL.Path, DisableTLS: apiURL.Scheme == "http", - Title: title, Token: n.gotifyAppToken, } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index be67d3b..cfca30e 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -15,13 +15,12 @@ const ( type msTeamsTypeNotifier struct { webHookURL string - levels []log.Level data bool } -func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { +func newMsTeamsNotifier(cmd *cobra.Command) t.ConvertibleNotifier { - flags := cmd.PersistentFlags() + flags := cmd.Flags() webHookURL, _ := flags.GetString("notification-msteams-hook") if len(webHookURL) <= 0 { @@ -30,7 +29,6 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con withData, _ := flags.GetBool("notification-msteams-data") n := &msTeamsTypeNotifier{ - levels: acceptedLogLevels, webHookURL: webHookURL, data: withData, } @@ -38,7 +36,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con return n } -func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { +func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) { webhookURL, err := url.Parse(n.webHookURL) if err != nil { return "", err @@ -50,7 +48,6 @@ func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, er } config.Color = ColorHex - config.Title = title return config.GetURL().String(), nil } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index fba5dc0..ff7b6b5 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -6,14 +6,13 @@ import ( "time" ty "github.com/containrrr/watchtower/pkg/types" - "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // NewNotifier creates and returns a new Notifier, using global configuration. func NewNotifier(c *cobra.Command) ty.Notifier { - f := c.PersistentFlags() + f := c.Flags() level, _ := f.GetString("notifications-level") logLevel, err := log.ParseLevel(level) @@ -21,25 +20,19 @@ func NewNotifier(c *cobra.Command) ty.Notifier { log.Fatalf("Notifications invalid log level: %s", err.Error()) } - levels := slackrus.LevelThreshold(logLevel) - // slackrus does not allow log level TRACE, even though it's an accepted log level for logrus - if len(levels) == 0 { - log.Fatalf("Unsupported notification log level provided: %s", level) - } - reportTemplate, _ := f.GetBool("notification-report") stdout, _ := f.GetBool("notification-log-stdout") tplString, _ := f.GetString("notification-template") urls, _ := f.GetStringArray("notification-url") data := GetTemplateData(c) - urls, delay := AppendLegacyUrls(urls, c, data.Title) + urls, delay := AppendLegacyUrls(urls, c) - return newShoutrrrNotifier(tplString, levels, !reportTemplate, data, delay, stdout, urls...) + return createNotifier(urls, logLevel, tplString, !reportTemplate, data, stdout, delay) } // AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags -func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string, time.Duration) { +func AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duration) { // Parse types and create notifiers. types, err := cmd.Flags().GetStringSlice("notifications") @@ -56,13 +49,13 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string switch t { case emailType: - legacyNotifier = newEmailNotifier(cmd, []log.Level{}) + legacyNotifier = newEmailNotifier(cmd) case slackType: - legacyNotifier = newSlackNotifier(cmd, []log.Level{}) + legacyNotifier = newSlackNotifier(cmd) case msTeamsType: - legacyNotifier = newMsTeamsNotifier(cmd, []log.Level{}) + legacyNotifier = newMsTeamsNotifier(cmd) case gotifyType: - legacyNotifier = newGotifyNotifier(cmd, []log.Level{}) + legacyNotifier = newGotifyNotifier(cmd) case shoutrrrType: continue default: @@ -71,7 +64,7 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string continue } - shoutrrrURL, err := legacyNotifier.GetURL(cmd, title) + shoutrrrURL, err := legacyNotifier.GetURL(cmd) if err != nil { log.Fatal("failed to create notification config: ", err) } diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 1b004dc..96d513c 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -3,7 +3,6 @@ package notifications_test import ( "fmt" "net/url" - "os" "time" "github.com/containrrr/watchtower/cmd" @@ -147,11 +146,9 @@ var _ = Describe("notifications", func() { channel := "123456789" token := "abvsihdbau" color := notifications.ColorInt - data := notifications.GetTemplateData(command) - title := url.QueryEscape(data.Title) username := "containrrrbot" iconURL := "https://containrrr.dev/watchtower-sq180.png" - 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) + expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=watchtower", token, channel, color) buildArgs := func(url string) []string { return []string{ "--notifications", @@ -172,7 +169,7 @@ var _ = Describe("notifications", func() { When("icon URL and username are specified", func() { It("should return the expected URL", func() { hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token) - expectedOutput := fmt.Sprintf("discord://%s@%s?avatar=%s&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&title=%s&username=%s", token, channel, url.QueryEscape(iconURL), color, title, username) + expectedOutput := fmt.Sprintf("discord://%s@%s?avatar=%s&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=%s", token, channel, url.QueryEscape(iconURL), color, username) expectedDelay := time.Duration(7) * time.Second args := []string{ "--notifications", @@ -199,8 +196,6 @@ var _ = Describe("notifications", func() { tokenB := "BBBBBBBBB" tokenC := "123456789123456789123456" color := url.QueryEscape(notifications.ColorHex) - data := notifications.GetTemplateData(command) - title := url.QueryEscape(data.Title) iconURL := "https://containrrr.dev/watchtower-sq180.png" iconEmoji := "whale" @@ -208,7 +203,7 @@ var _ = Describe("notifications", func() { It("should return the expected URL", 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) + expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL)) expectedDelay := time.Duration(7) * time.Second args := []string{ @@ -231,7 +226,7 @@ var _ = Describe("notifications", func() { When("icon emoji is specified", func() { It("should return the expected URL", 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, iconEmoji, title) + expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, iconEmoji) args := []string{ "--notifications", @@ -258,10 +253,8 @@ var _ = Describe("notifications", func() { token := "aaa" host := "shoutrrr.local" - data := notifications.GetTemplateData(command) - title := url.QueryEscape(data.Title) - expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title) + expectedOutput := fmt.Sprintf("gotify://%s/%s?title=", host, token) args := []string{ "--notifications", @@ -287,11 +280,9 @@ var _ = Describe("notifications", func() { tokenB := "33333333012222222222333333333344" tokenC := "44444444-4444-4444-8444-cccccccccccc" color := url.QueryEscape(notifications.ColorHex) - 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) + expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s", tokenA, tokenB, tokenC, color) args := []string{ "--notifications", @@ -362,18 +353,12 @@ var _ = Describe("notifications", func() { }) 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/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=%s&toaddresses=%s" + var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=&toaddresses=%s" return fmt.Sprintf(template, url.QueryEscape(username), url.QueryEscape(password), host, port, auth, url.QueryEscape(from), - url.QueryEscape(subject), url.QueryEscape(to)) } @@ -385,8 +370,7 @@ func testURL(args []string, expectedURL string, expectedDelay time.Duration) { Expect(command.ParseFlags(args)).To(Succeed()) - data := notifications.GetTemplateData(command) - urls, delay := notifications.AppendLegacyUrls([]string{}, command, data.Title) + urls, delay := notifications.AppendLegacyUrls([]string{}, command) Expect(urls).To(ContainElement(expectedURL)) Expect(delay).To(Equal(expectedDelay)) diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index e816cf7..47141e8 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -32,13 +32,15 @@ type shoutrrrTypeNotifier struct { Urls []string Router router entries []*log.Entry - logLevels []log.Level + logLevel log.Level template *template.Template messages chan string done chan bool legacyTemplate bool params *types.Params data StaticData + receiving bool + delay time.Duration } // GetScheme returns the scheme part of a Shoutrrr URL @@ -59,18 +61,24 @@ func (n *shoutrrrTypeNotifier) GetNames() []string { return names } -func newShoutrrrNotifier(tplString string, levels []log.Level, legacy bool, data StaticData, delay time.Duration, stdout bool, urls ...string) t.Notifier { +// GetNames returns a list of URLs for notification services that has been added +func (n *shoutrrrTypeNotifier) GetURLs() []string { + return n.Urls +} - notifier := createNotifier(urls, levels, tplString, legacy, data, stdout) - log.AddHook(notifier) +// AddLogHook adds the notifier as a receiver of log messages and starts a go func for processing them +func (n *shoutrrrTypeNotifier) AddLogHook() { + if n.receiving { + return + } + n.receiving = true + log.AddHook(n) // Do the sending in a separate goroutine so we don't block the main process. - go sendNotifications(notifier, delay) - - return notifier + go sendNotifications(n) } -func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool, data StaticData, stdout bool) *shoutrrrTypeNotifier { +func createNotifier(urls []string, level log.Level, tplString string, legacy bool, data StaticData, stdout bool, delay time.Duration) *shoutrrrTypeNotifier { tpl, err := getShoutrrrTemplate(tplString, legacy) if err != nil { log.Errorf("Could not use configured notification template: %s. Using default template", err) @@ -97,7 +105,7 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy Router: r, messages: make(chan string, 1), done: make(chan bool), - logLevels: levels, + logLevel: level, template: tpl, legacyTemplate: legacy, data: data, @@ -105,9 +113,9 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy } } -func sendNotifications(n *shoutrrrTypeNotifier, delay time.Duration) { +func sendNotifications(n *shoutrrrTypeNotifier) { for msg := range n.messages { - time.Sleep(delay) + time.Sleep(n.delay) errs := n.Router.Send(msg, n.params) for i, err := range errs { @@ -180,7 +188,7 @@ func (n *shoutrrrTypeNotifier) Close() { // Levels return what log levels trigger notifications func (n *shoutrrrTypeNotifier) Levels() []log.Level { - return n.logLevels + return log.AllLevels[:n.logLevel+1] } // Fire is the hook that logrus calls on a new log message diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index 0a10eb1..703958b 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -14,7 +14,7 @@ import ( "github.com/spf13/cobra" ) -var allButTrace = logrus.AllLevels[0:logrus.TraceLevel] +var allButTrace = logrus.DebugLevel var legacyMockData = Data{ Entries: []*logrus.Entry{ @@ -83,6 +83,30 @@ updt1 (mock/updt1:latest): Updated }) }) + When("adding a log hook", func() { + When("it has not been added before", func() { + It("should be added to the logrus hooks", func() { + level := logrus.TraceLevel + hooksBefore := len(logrus.StandardLogger().Hooks[level]) + shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second) + shoutrrr.AddLogHook() + hooksAfter := len(logrus.StandardLogger().Hooks[level]) + Expect(hooksAfter).To(BeNumerically(">", hooksBefore)) + }) + }) + When("it is being added a second time", func() { + It("should not be added to the logrus hooks", func() { + level := logrus.TraceLevel + shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second) + shoutrrr.AddLogHook() + hooksBefore := len(logrus.StandardLogger().Hooks[level]) + shoutrrr.AddLogHook() + hooksAfter := len(logrus.StandardLogger().Hooks[level]) + Expect(hooksAfter).To(Equal(hooksBefore)) + }) + }) + }) + When("using legacy templates", func() { When("no custom template is provided", func() { @@ -90,7 +114,7 @@ updt1 (mock/updt1:latest): Updated cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) - shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{}, false) + shoutrrr := createNotifier([]string{}, logrus.TraceLevel, "", true, StaticData{}, false, time.Second) entries := []*logrus.Entry{ { @@ -245,7 +269,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, StaticData{}, time.Duration(0), false, "logger://") + shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0)) shoutrrr.StartNotification() shoutrrr.SendNotification(nil) Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`)) @@ -253,7 +277,8 @@ Turns out everything is on fire }) When("at least one message is queued", func() { It("should send a notification", func() { - shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), false, "logger://") + shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0)) + shoutrrr.AddLogHook() shoutrrr.StartNotification() logrus.Info("This log message is sponsored by ContainrrrVPN") shoutrrr.SendNotification(nil) @@ -267,7 +292,7 @@ Turns out everything is on fire shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{ Host: "test.host", Title: "", - }, false) + }, false, time.Second) _, found := shoutrrr.params.Title() Expect(found).ToNot(BeTrue()) }) @@ -321,13 +346,14 @@ func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *b Router: router, legacyTemplate: legacy, params: &types.Params{}, + delay: time.Duration(0), } entry := &logrus.Entry{ Message: "foo bar", } - go sendNotifications(shoutrrr, time.Duration(0)) + go sendNotifications(shoutrrr) shoutrrr.StartNotification() _ = shoutrrr.Fire(entry) diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 34d21a3..9118527 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -6,7 +6,6 @@ import ( shoutrrrDisco "github.com/containrrr/shoutrrr/pkg/services/discord" shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack" t "github.com/containrrr/watchtower/pkg/types" - "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -16,11 +15,15 @@ const ( ) type slackTypeNotifier struct { - slackrus.SlackrusHook + HookURL string + Username string + Channel string + IconEmoji string + IconURL string } -func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { - flags := c.PersistentFlags() +func newSlackNotifier(c *cobra.Command) t.ConvertibleNotifier { + flags := c.Flags() hookURL, _ := flags.GetString("notification-slack-hook-url") userName, _ := flags.GetString("notification-slack-identifier") @@ -29,19 +32,16 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert iconURL, _ := flags.GetString("notification-slack-icon-url") n := &slackTypeNotifier{ - SlackrusHook: slackrus.SlackrusHook{ - HookURL: hookURL, - Username: userName, - Channel: channel, - IconEmoji: emoji, - IconURL: iconURL, - AcceptedLevels: acceptedLogLevels, - }, + HookURL: hookURL, + Username: userName, + Channel: channel, + IconEmoji: emoji, + IconURL: iconURL, } return n } -func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { +func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) { trimmedURL := strings.TrimRight(s.HookURL, "/") trimmedURL = strings.TrimPrefix(trimmedURL, "https://") parts := strings.Split(trimmedURL, "/") @@ -52,7 +52,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro WebhookID: parts[len(parts)-3], Token: parts[len(parts)-2], Color: ColorInt, - Title: title, SplitLines: true, Username: s.Username, } @@ -70,7 +69,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro BotName: s.Username, Color: ColorHex, Channel: "webhook", - Title: title, } if s.IconURL != "" { diff --git a/pkg/types/convertible_notifier.go b/pkg/types/convertible_notifier.go index 37d8872..82d7b7b 100644 --- a/pkg/types/convertible_notifier.go +++ b/pkg/types/convertible_notifier.go @@ -1,16 +1,17 @@ package types import ( - "github.com/spf13/cobra" "time" + + "github.com/spf13/cobra" ) // ConvertibleNotifier is a notifier capable of creating a shoutrrr URL type ConvertibleNotifier interface { - GetURL(c *cobra.Command, title string) (string, error) + GetURL(c *cobra.Command) (string, error) } // DelayNotifier is a notifier that might need to be delayed before sending notifications type DelayNotifier interface { GetDelay() time.Duration -} \ No newline at end of file +} diff --git a/pkg/types/notifier.go b/pkg/types/notifier.go index ccb2cb6..478a4c4 100644 --- a/pkg/types/notifier.go +++ b/pkg/types/notifier.go @@ -4,6 +4,8 @@ package types type Notifier interface { StartNotification() SendNotification(Report) + AddLogHook() GetNames() []string + GetURLs() []string Close() }