preparations for soft deprecation of legacy notification args (#1377)

Co-authored-by: Simon Aronsson <simme@arcticbit.se>
pull/1451/head
nils måsén 2 years ago committed by GitHub
parent 2102a056de
commit cb555f539d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 := "<CONTAINER>"
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...))
}

@ -66,6 +66,7 @@ func init() {
// Execute the root func and exit in case of errors // Execute the root func and exit in case of errors
func Execute() { func Execute() {
rootCmd.AddCommand(notifyUpgradeCommand)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -139,6 +140,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
}) })
notifier = notifications.NewNotifier(cmd) notifier = notifications.NewNotifier(cmd)
notifier.AddLogHook()
} }
// Run is the main execution flow of the command // Run is the main execution flow of the command

@ -1,15 +1,7 @@
# Notifications # Notifications
Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging 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 system, [logrus](http://github.com/sirupsen/logrus).
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)
!!! note "Using multiple notifications with environment variables" !!! 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 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-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. - `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 ### 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`. 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
```

@ -8,7 +8,6 @@ require (
github.com/docker/distribution v2.8.1+incompatible github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.20+incompatible github.com/docker/docker v20.10.20+incompatible
github.com/docker/go-connections v0.4.0 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/ginkgo v1.16.5
github.com/onsi/gomega v1.22.1 github.com/onsi/gomega v1.22.1
github.com/prometheus/client_golang v1.13.0 github.com/prometheus/client_golang v1.13.0
@ -36,7 +35,6 @@ require (
github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-cmp v0.5.8 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // 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/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect

@ -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/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 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA=
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= 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/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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=

@ -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])
}

@ -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())
})
})
})
//

@ -1,3 +1,4 @@
// Package container contains code related to dealing with docker containers
package container package container
import ( import (

@ -15,17 +15,16 @@ const (
) )
type emailTypeNotifier struct { type emailTypeNotifier struct {
From, To string From, To string
Server, User, Password, SubjectTag string Server, User, Password string
Port int Port int
tlsSkipVerify bool tlsSkipVerify bool
entries []*log.Entry entries []*log.Entry
logLevels []log.Level delay time.Duration
delay time.Duration
} }
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { func newEmailNotifier(c *cobra.Command) t.ConvertibleNotifier {
flags := c.PersistentFlags() flags := c.Flags()
from, _ := flags.GetString("notification-email-from") from, _ := flags.GetString("notification-email-from")
to, _ := flags.GetString("notification-email-to") 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") port, _ := flags.GetInt("notification-email-server-port")
tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify")
delay, _ := flags.GetInt("notification-email-delay") delay, _ := flags.GetInt("notification-email-delay")
subjecttag, _ := flags.GetString("notification-email-subjecttag")
n := &emailTypeNotifier{ n := &emailTypeNotifier{
entries: []*log.Entry{}, entries: []*log.Entry{},
@ -46,22 +44,19 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
Password: password, Password: password,
Port: port, Port: port,
tlsSkipVerify: tlsSkipVerify, tlsSkipVerify: tlsSkipVerify,
logLevels: acceptedLogLevels,
delay: time.Duration(delay) * time.Second, delay: time.Duration(delay) * time.Second,
SubjectTag: subjecttag,
} }
return n 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{ conf := &shoutrrrSmtp.Config{
FromAddress: e.From, FromAddress: e.From,
FromName: "Watchtower", FromName: "Watchtower",
ToAddresses: []string{e.To}, ToAddresses: []string{e.To},
Port: uint16(e.Port), Port: uint16(e.Port),
Host: e.Server, Host: e.Server,
Subject: e.getSubject(c, title),
Username: e.User, Username: e.User,
Password: e.Password, Password: e.Password,
UseStartTLS: !e.tlsSkipVerify, UseStartTLS: !e.tlsSkipVerify,
@ -84,11 +79,3 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
func (e *emailTypeNotifier) GetDelay() time.Duration { func (e *emailTypeNotifier) GetDelay() time.Duration {
return e.delay return e.delay
} }
func (e *emailTypeNotifier) getSubject(_ *cobra.Command, title string) string {
if e.SubjectTag != "" {
return e.SubjectTag + " " + title
}
return title
}

@ -19,11 +19,10 @@ type gotifyTypeNotifier struct {
gotifyURL string gotifyURL string
gotifyAppToken string gotifyAppToken string
gotifyInsecureSkipVerify bool gotifyInsecureSkipVerify bool
logLevels []log.Level
} }
func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier { func newGotifyNotifier(c *cobra.Command) t.ConvertibleNotifier {
flags := c.PersistentFlags() flags := c.Flags()
apiURL := getGotifyURL(flags) apiURL := getGotifyURL(flags)
token := getGotifyToken(flags) token := getGotifyToken(flags)
@ -34,7 +33,6 @@ func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifi
gotifyURL: apiURL, gotifyURL: apiURL,
gotifyAppToken: token, gotifyAppToken: token,
gotifyInsecureSkipVerify: skipVerify, gotifyInsecureSkipVerify: skipVerify,
logLevels: levels,
} }
return n return n
@ -62,7 +60,7 @@ func getGotifyURL(flags *pflag.FlagSet) string {
return gotifyURL 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) apiURL, err := url.Parse(n.gotifyURL)
if err != nil { if err != nil {
return "", err return "", err
@ -72,7 +70,6 @@ func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, err
Host: apiURL.Host, Host: apiURL.Host,
Path: apiURL.Path, Path: apiURL.Path,
DisableTLS: apiURL.Scheme == "http", DisableTLS: apiURL.Scheme == "http",
Title: title,
Token: n.gotifyAppToken, Token: n.gotifyAppToken,
} }

@ -15,13 +15,12 @@ const (
type msTeamsTypeNotifier struct { type msTeamsTypeNotifier struct {
webHookURL string webHookURL string
levels []log.Level
data bool 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") webHookURL, _ := flags.GetString("notification-msteams-hook")
if len(webHookURL) <= 0 { if len(webHookURL) <= 0 {
@ -30,7 +29,6 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con
withData, _ := flags.GetBool("notification-msteams-data") withData, _ := flags.GetBool("notification-msteams-data")
n := &msTeamsTypeNotifier{ n := &msTeamsTypeNotifier{
levels: acceptedLogLevels,
webHookURL: webHookURL, webHookURL: webHookURL,
data: withData, data: withData,
} }
@ -38,7 +36,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con
return n 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) webhookURL, err := url.Parse(n.webHookURL)
if err != nil { if err != nil {
return "", err return "", err
@ -50,7 +48,6 @@ func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, er
} }
config.Color = ColorHex config.Color = ColorHex
config.Title = title
return config.GetURL().String(), nil return config.GetURL().String(), nil
} }

@ -6,14 +6,13 @@ import (
"time" "time"
ty "github.com/containrrr/watchtower/pkg/types" ty "github.com/containrrr/watchtower/pkg/types"
"github.com/johntdyer/slackrus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// NewNotifier creates and returns a new Notifier, using global configuration. // NewNotifier creates and returns a new Notifier, using global configuration.
func NewNotifier(c *cobra.Command) ty.Notifier { func NewNotifier(c *cobra.Command) ty.Notifier {
f := c.PersistentFlags() f := c.Flags()
level, _ := f.GetString("notifications-level") level, _ := f.GetString("notifications-level")
logLevel, err := log.ParseLevel(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()) 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") reportTemplate, _ := f.GetBool("notification-report")
stdout, _ := f.GetBool("notification-log-stdout") stdout, _ := f.GetBool("notification-log-stdout")
tplString, _ := f.GetString("notification-template") tplString, _ := f.GetString("notification-template")
urls, _ := f.GetStringArray("notification-url") urls, _ := f.GetStringArray("notification-url")
data := GetTemplateData(c) 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 // 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. // Parse types and create notifiers.
types, err := cmd.Flags().GetStringSlice("notifications") types, err := cmd.Flags().GetStringSlice("notifications")
@ -56,13 +49,13 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string
switch t { switch t {
case emailType: case emailType:
legacyNotifier = newEmailNotifier(cmd, []log.Level{}) legacyNotifier = newEmailNotifier(cmd)
case slackType: case slackType:
legacyNotifier = newSlackNotifier(cmd, []log.Level{}) legacyNotifier = newSlackNotifier(cmd)
case msTeamsType: case msTeamsType:
legacyNotifier = newMsTeamsNotifier(cmd, []log.Level{}) legacyNotifier = newMsTeamsNotifier(cmd)
case gotifyType: case gotifyType:
legacyNotifier = newGotifyNotifier(cmd, []log.Level{}) legacyNotifier = newGotifyNotifier(cmd)
case shoutrrrType: case shoutrrrType:
continue continue
default: default:
@ -71,7 +64,7 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string
continue continue
} }
shoutrrrURL, err := legacyNotifier.GetURL(cmd, title) shoutrrrURL, err := legacyNotifier.GetURL(cmd)
if err != nil { if err != nil {
log.Fatal("failed to create notification config: ", err) log.Fatal("failed to create notification config: ", err)
} }

@ -3,7 +3,6 @@ package notifications_test
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"os"
"time" "time"
"github.com/containrrr/watchtower/cmd" "github.com/containrrr/watchtower/cmd"
@ -147,11 +146,9 @@ var _ = Describe("notifications", func() {
channel := "123456789" channel := "123456789"
token := "abvsihdbau" token := "abvsihdbau"
color := notifications.ColorInt color := notifications.ColorInt
data := notifications.GetTemplateData(command)
title := url.QueryEscape(data.Title)
username := "containrrrbot" username := "containrrrbot"
iconURL := "https://containrrr.dev/watchtower-sq180.png" 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 { buildArgs := func(url string) []string {
return []string{ return []string{
"--notifications", "--notifications",
@ -172,7 +169,7 @@ var _ = Describe("notifications", func() {
When("icon URL and username are specified", func() { When("icon URL and username are specified", func() {
It("should return the expected URL", func() { It("should return the expected URL", func() {
hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token) 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 expectedDelay := time.Duration(7) * time.Second
args := []string{ args := []string{
"--notifications", "--notifications",
@ -199,8 +196,6 @@ var _ = Describe("notifications", func() {
tokenB := "BBBBBBBBB" tokenB := "BBBBBBBBB"
tokenC := "123456789123456789123456" tokenC := "123456789123456789123456"
color := url.QueryEscape(notifications.ColorHex) color := url.QueryEscape(notifications.ColorHex)
data := notifications.GetTemplateData(command)
title := url.QueryEscape(data.Title)
iconURL := "https://containrrr.dev/watchtower-sq180.png" iconURL := "https://containrrr.dev/watchtower-sq180.png"
iconEmoji := "whale" iconEmoji := "whale"
@ -208,7 +203,7 @@ var _ = Describe("notifications", func() {
It("should return the expected URL", func() { It("should return the expected URL", func() {
hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) 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 expectedDelay := time.Duration(7) * time.Second
args := []string{ args := []string{
@ -231,7 +226,7 @@ var _ = Describe("notifications", func() {
When("icon emoji is specified", func() { When("icon emoji is specified", func() {
It("should return the expected URL", func() { It("should return the expected URL", func() {
hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) 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{ args := []string{
"--notifications", "--notifications",
@ -258,10 +253,8 @@ var _ = Describe("notifications", func() {
token := "aaa" token := "aaa"
host := "shoutrrr.local" 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{ args := []string{
"--notifications", "--notifications",
@ -287,11 +280,9 @@ var _ = Describe("notifications", func() {
tokenB := "33333333012222222222333333333344" tokenB := "33333333012222222222333333333344"
tokenC := "44444444-4444-4444-8444-cccccccccccc" tokenC := "44444444-4444-4444-8444-cccccccccccc"
color := url.QueryEscape(notifications.ColorHex) 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) 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{ args := []string{
"--notifications", "--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 { func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string {
hostname, err := os.Hostname() var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=&toaddresses=%s"
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"
return fmt.Sprintf(template, return fmt.Sprintf(template,
url.QueryEscape(username), url.QueryEscape(username),
url.QueryEscape(password), url.QueryEscape(password),
host, port, auth, host, port, auth,
url.QueryEscape(from), url.QueryEscape(from),
url.QueryEscape(subject),
url.QueryEscape(to)) url.QueryEscape(to))
} }
@ -385,8 +370,7 @@ func testURL(args []string, expectedURL string, expectedDelay time.Duration) {
Expect(command.ParseFlags(args)).To(Succeed()) Expect(command.ParseFlags(args)).To(Succeed())
data := notifications.GetTemplateData(command) urls, delay := notifications.AppendLegacyUrls([]string{}, command)
urls, delay := notifications.AppendLegacyUrls([]string{}, command, data.Title)
Expect(urls).To(ContainElement(expectedURL)) Expect(urls).To(ContainElement(expectedURL))
Expect(delay).To(Equal(expectedDelay)) Expect(delay).To(Equal(expectedDelay))

@ -32,13 +32,15 @@ type shoutrrrTypeNotifier struct {
Urls []string Urls []string
Router router Router router
entries []*log.Entry entries []*log.Entry
logLevels []log.Level logLevel log.Level
template *template.Template template *template.Template
messages chan string messages chan string
done chan bool done chan bool
legacyTemplate bool legacyTemplate bool
params *types.Params params *types.Params
data StaticData data StaticData
receiving bool
delay time.Duration
} }
// GetScheme returns the scheme part of a Shoutrrr URL // GetScheme returns the scheme part of a Shoutrrr URL
@ -59,18 +61,24 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
return names 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) // AddLogHook adds the notifier as a receiver of log messages and starts a go func for processing them
log.AddHook(notifier) 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. // Do the sending in a separate goroutine so we don't block the main process.
go sendNotifications(notifier, delay) go sendNotifications(n)
return notifier
} }
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) tpl, err := getShoutrrrTemplate(tplString, legacy)
if err != nil { if err != nil {
log.Errorf("Could not use configured notification template: %s. Using default template", err) 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, Router: r,
messages: make(chan string, 1), messages: make(chan string, 1),
done: make(chan bool), done: make(chan bool),
logLevels: levels, logLevel: level,
template: tpl, template: tpl,
legacyTemplate: legacy, legacyTemplate: legacy,
data: data, 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 { for msg := range n.messages {
time.Sleep(delay) time.Sleep(n.delay)
errs := n.Router.Send(msg, n.params) errs := n.Router.Send(msg, n.params)
for i, err := range errs { for i, err := range errs {
@ -180,7 +188,7 @@ func (n *shoutrrrTypeNotifier) Close() {
// Levels return what log levels trigger notifications // Levels return what log levels trigger notifications
func (n *shoutrrrTypeNotifier) Levels() []log.Level { 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 // Fire is the hook that logrus calls on a new log message

@ -14,7 +14,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var allButTrace = logrus.AllLevels[0:logrus.TraceLevel] var allButTrace = logrus.DebugLevel
var legacyMockData = Data{ var legacyMockData = Data{
Entries: []*logrus.Entry{ 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("using legacy templates", func() {
When("no custom template is provided", func() { When("no custom template is provided", func() {
@ -90,7 +114,7 @@ updt1 (mock/updt1:latest): Updated
cmd := new(cobra.Command) cmd := new(cobra.Command)
flags.RegisterNotificationFlags(cmd) flags.RegisterNotificationFlags(cmd)
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{}, false) shoutrrr := createNotifier([]string{}, logrus.TraceLevel, "", true, StaticData{}, false, time.Second)
entries := []*logrus.Entry{ entries := []*logrus.Entry{
{ {
@ -245,7 +269,7 @@ Turns out everything is on fire
When("batching notifications", func() { When("batching notifications", func() {
When("no messages are queued", func() { When("no messages are queued", func() {
It("should not send any notification", 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.StartNotification()
shoutrrr.SendNotification(nil) shoutrrr.SendNotification(nil)
Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`)) Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))
@ -253,7 +277,8 @@ Turns out everything is on fire
}) })
When("at least one message is queued", func() { When("at least one message is queued", func() {
It("should send a notification", 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() shoutrrr.StartNotification()
logrus.Info("This log message is sponsored by ContainrrrVPN") logrus.Info("This log message is sponsored by ContainrrrVPN")
shoutrrr.SendNotification(nil) shoutrrr.SendNotification(nil)
@ -267,7 +292,7 @@ Turns out everything is on fire
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{ shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{
Host: "test.host", Host: "test.host",
Title: "", Title: "",
}, false) }, false, time.Second)
_, found := shoutrrr.params.Title() _, found := shoutrrr.params.Title()
Expect(found).ToNot(BeTrue()) Expect(found).ToNot(BeTrue())
}) })
@ -321,13 +346,14 @@ func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *b
Router: router, Router: router,
legacyTemplate: legacy, legacyTemplate: legacy,
params: &types.Params{}, params: &types.Params{},
delay: time.Duration(0),
} }
entry := &logrus.Entry{ entry := &logrus.Entry{
Message: "foo bar", Message: "foo bar",
} }
go sendNotifications(shoutrrr, time.Duration(0)) go sendNotifications(shoutrrr)
shoutrrr.StartNotification() shoutrrr.StartNotification()
_ = shoutrrr.Fire(entry) _ = shoutrrr.Fire(entry)

@ -6,7 +6,6 @@ import (
shoutrrrDisco "github.com/containrrr/shoutrrr/pkg/services/discord" shoutrrrDisco "github.com/containrrr/shoutrrr/pkg/services/discord"
shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack" shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack"
t "github.com/containrrr/watchtower/pkg/types" t "github.com/containrrr/watchtower/pkg/types"
"github.com/johntdyer/slackrus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -16,11 +15,15 @@ const (
) )
type slackTypeNotifier struct { 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 { func newSlackNotifier(c *cobra.Command) t.ConvertibleNotifier {
flags := c.PersistentFlags() flags := c.Flags()
hookURL, _ := flags.GetString("notification-slack-hook-url") hookURL, _ := flags.GetString("notification-slack-hook-url")
userName, _ := flags.GetString("notification-slack-identifier") 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") iconURL, _ := flags.GetString("notification-slack-icon-url")
n := &slackTypeNotifier{ n := &slackTypeNotifier{
SlackrusHook: slackrus.SlackrusHook{ HookURL: hookURL,
HookURL: hookURL, Username: userName,
Username: userName, Channel: channel,
Channel: channel, IconEmoji: emoji,
IconEmoji: emoji, IconURL: iconURL,
IconURL: iconURL,
AcceptedLevels: acceptedLogLevels,
},
} }
return n 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.TrimRight(s.HookURL, "/")
trimmedURL = strings.TrimPrefix(trimmedURL, "https://") trimmedURL = strings.TrimPrefix(trimmedURL, "https://")
parts := strings.Split(trimmedURL, "/") parts := strings.Split(trimmedURL, "/")
@ -52,7 +52,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
WebhookID: parts[len(parts)-3], WebhookID: parts[len(parts)-3],
Token: parts[len(parts)-2], Token: parts[len(parts)-2],
Color: ColorInt, Color: ColorInt,
Title: title,
SplitLines: true, SplitLines: true,
Username: s.Username, Username: s.Username,
} }
@ -70,7 +69,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
BotName: s.Username, BotName: s.Username,
Color: ColorHex, Color: ColorHex,
Channel: "webhook", Channel: "webhook",
Title: title,
} }
if s.IconURL != "" { if s.IconURL != "" {

@ -1,16 +1,17 @@
package types package types
import ( import (
"github.com/spf13/cobra"
"time" "time"
"github.com/spf13/cobra"
) )
// ConvertibleNotifier is a notifier capable of creating a shoutrrr URL // ConvertibleNotifier is a notifier capable of creating a shoutrrr URL
type ConvertibleNotifier interface { 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 // DelayNotifier is a notifier that might need to be delayed before sending notifications
type DelayNotifier interface { type DelayNotifier interface {
GetDelay() time.Duration GetDelay() time.Duration
} }

@ -4,6 +4,8 @@ package types
type Notifier interface { type Notifier interface {
StartNotification() StartNotification()
SendNotification(Report) SendNotification(Report)
AddLogHook()
GetNames() []string GetNames() []string
GetURLs() []string
Close() Close()
} }

Loading…
Cancel
Save