fix(notifications): title customization (#1219)

pull/1277/head
nils måsén 2 years ago committed by GitHub
parent e9c83af533
commit 2f4d58776d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -28,6 +28,8 @@ comma-separated list of values to the `--notifications` option
- `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname.
- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATION_DELAY`): Delay before sending notifications expressed in seconds.
- Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument.
- `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
@ -43,7 +45,7 @@ To receive notifications by email, the following command-line options, or their
- `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.
- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used.
- `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds.
- `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers.
- `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. **NOTE:** This will affect all notification types.
Example:

@ -326,6 +326,16 @@ Should only be used for testing.`)
viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"),
"Use the session report as the notification template data")
flags.StringP(
"notification-title-tag",
"",
viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
"Title prefix tag for notifications")
flags.Bool("notification-skip-title",
viper.GetBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
"Do not pass the title param to notifications")
flags.String(
"warn-on-head-failure",
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),

@ -2,6 +2,7 @@ package notifications
import (
"os"
"strings"
"time"
ty "github.com/containrrr/watchtower/pkg/types"
@ -30,10 +31,10 @@ func NewNotifier(c *cobra.Command) ty.Notifier {
tplString, _ := f.GetString("notification-template")
urls, _ := f.GetStringArray("notification-url")
hostname := GetHostname(c)
urls, delay := AppendLegacyUrls(urls, c, GetTitle(hostname))
data := GetTemplateData(c)
urls, delay := AppendLegacyUrls(urls, c, data.Title)
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, hostname, delay, urls...)
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, data, delay, urls...)
}
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags
@ -99,28 +100,50 @@ func GetDelay(c *cobra.Command, legacyDelay time.Duration) time.Duration {
return time.Duration(0)
}
// GetTitle returns a common notification title with hostname appended
func GetTitle(hostname string) string {
title := "Watchtower updates"
// GetTitle formats the title based on the passed hostname and tag
func GetTitle(hostname string, tag string) string {
tb := strings.Builder{}
if tag != "" {
tb.WriteRune('[')
tb.WriteString(tag)
tb.WriteRune(']')
tb.WriteRune(' ')
}
tb.WriteString("Watchtower updates")
if hostname != "" {
title += " on " + hostname
tb.WriteString(" on ")
tb.WriteString(hostname)
}
return title
}
// GetHostname returns the hostname as set by args or resolved from OS
func GetHostname(c *cobra.Command) string {
return tb.String()
}
// GetTemplateData populates the static notification data from flags and environment
func GetTemplateData(c *cobra.Command) StaticData {
f := c.PersistentFlags()
hostname, _ := f.GetString("notifications-hostname")
if hostname == "" {
hostname, _ = os.Hostname()
}
if hostname != "" {
return hostname
} else if hostname, err := os.Hostname(); err == nil {
return hostname
title := ""
if skip, _ := f.GetBool("notification-skip-title"); !skip {
tag, _ := f.GetString("notification-title-tag")
if tag == "" {
// For legacy email support
tag, _ = f.GetString("notification-email-subjecttag")
}
title = GetTitle(hostname, tag)
}
return ""
return StaticData{
Host: hostname,
Title: title,
}
}
// ColorHex is the default notification color used for services that support it (formatted as a CSS hex string)

@ -39,17 +39,58 @@ var _ = Describe("notifications", func() {
"test.host",
})
Expect(err).NotTo(HaveOccurred())
hostname := notifications.GetHostname(command)
title := notifications.GetTitle(hostname)
data := notifications.GetTemplateData(command)
title := data.Title
Expect(title).To(Equal("Watchtower updates on test.host"))
})
})
When("no hostname can be resolved", func() {
It("should use the default simple title", func() {
title := notifications.GetTitle("")
title := notifications.GetTitle("", "")
Expect(title).To(Equal("Watchtower updates"))
})
})
When("title tag is set", func() {
It("should use the prefix in the title", func() {
command := cmd.NewRootCommand()
flags.RegisterNotificationFlags(command)
Expect(command.ParseFlags([]string{
"--notification-title-tag",
"PREFIX",
})).To(Succeed())
data := notifications.GetTemplateData(command)
Expect(data.Title).To(HavePrefix("[PREFIX]"))
})
})
When("legacy email tag is set", func() {
It("should use the prefix in the title", func() {
command := cmd.NewRootCommand()
flags.RegisterNotificationFlags(command)
Expect(command.ParseFlags([]string{
"--notification-email-subjecttag",
"PREFIX",
})).To(Succeed())
data := notifications.GetTemplateData(command)
Expect(data.Title).To(HavePrefix("[PREFIX]"))
})
})
When("the skip title flag is set", func() {
It("should return an empty title", func() {
command := cmd.NewRootCommand()
flags.RegisterNotificationFlags(command)
Expect(command.ParseFlags([]string{
"--notification-skip-title",
})).To(Succeed())
data := notifications.GetTemplateData(command)
Expect(data.Title).To(BeEmpty())
})
})
When("no delay is defined", func() {
It("should use the default delay", func() {
command := cmd.NewRootCommand()
@ -106,8 +147,8 @@ var _ = Describe("notifications", func() {
channel := "123456789"
token := "abvsihdbau"
color := notifications.ColorInt
hostname := notifications.GetHostname(command)
title := url.QueryEscape(notifications.GetTitle(hostname))
data := notifications.GetTemplateData(command)
title := url.QueryEscape(data.Title)
expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&title=%s&username=watchtower", token, channel, color, title)
buildArgs := func(url string) []string {
return []string{
@ -135,8 +176,8 @@ var _ = Describe("notifications", func() {
tokenB := "BBBBBBBBB"
tokenC := "123456789123456789123456"
color := url.QueryEscape(notifications.ColorHex)
hostname := notifications.GetHostname(command)
title := url.QueryEscape(notifications.GetTitle(hostname))
data := notifications.GetTemplateData(command)
title := url.QueryEscape(data.Title)
iconURL := "https://containrrr.dev/watchtower-sq180.png"
iconEmoji := "whale"
@ -194,8 +235,8 @@ var _ = Describe("notifications", func() {
token := "aaa"
host := "shoutrrr.local"
hostname := notifications.GetHostname(command)
title := url.QueryEscape(notifications.GetTitle(hostname))
data := notifications.GetTemplateData(command)
title := url.QueryEscape(data.Title)
expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title)
@ -223,8 +264,8 @@ var _ = Describe("notifications", func() {
tokenB := "33333333012222222222333333333344"
tokenC := "44444444-4444-4444-8444-cccccccccccc"
color := url.QueryEscape(notifications.ColorHex)
hostname := notifications.GetHostname(command)
title := url.QueryEscape(notifications.GetTitle(hostname))
data := notifications.GetTemplateData(command)
title := url.QueryEscape(data.Title)
hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC)
expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title)
@ -319,14 +360,10 @@ func testURL(args []string, expectedURL string, expectedDelay time.Duration) {
command := cmd.NewRootCommand()
flags.RegisterNotificationFlags(command)
err := command.ParseFlags(args)
Expect(err).NotTo(HaveOccurred())
Expect(command.ParseFlags(args)).To(Succeed())
hostname := notifications.GetHostname(command)
title := notifications.GetTitle(hostname)
urls, delay := notifications.AppendLegacyUrls([]string{}, command, title)
Expect(err).NotTo(HaveOccurred())
data := notifications.GetTemplateData(command)
urls, delay := notifications.AppendLegacyUrls([]string{}, command, data.Title)
Expect(urls).To(ContainElement(expectedURL))
Expect(delay).To(Equal(expectedDelay))

@ -58,7 +58,7 @@ type shoutrrrTypeNotifier struct {
done chan bool
legacyTemplate bool
params *types.Params
hostname string
data StaticData
}
// GetScheme returns the scheme part of a Shoutrrr URL
@ -79,11 +79,9 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
return names
}
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, hostname string, delay time.Duration, urls ...string) t.Notifier {
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, data StaticData, delay time.Duration, urls ...string) t.Notifier {
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy)
notifier.hostname = hostname
notifier.params = &types.Params{"title": GetTitle(hostname)}
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy, data)
log.AddHook(notifier)
// Do the sending in a separate goroutine so we don't block the main process.
@ -92,7 +90,7 @@ func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy
return notifier
}
func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool) *shoutrrrTypeNotifier {
func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool, data StaticData) *shoutrrrTypeNotifier {
tpl, err := getShoutrrrTemplate(tplString, legacy)
if err != nil {
log.Errorf("Could not use configured notification template: %s. Using default template", err)
@ -104,6 +102,11 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy
log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
}
params := &types.Params{}
if data.Title != "" {
params.SetTitle(data.Title)
}
return &shoutrrrTypeNotifier{
Urls: urls,
Router: r,
@ -112,6 +115,8 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy
logLevels: levels,
template: tpl,
legacyTemplate: legacy,
data: data,
params: params,
}
}
@ -149,9 +154,7 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) {
}
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) {
title, _ := n.params.Title()
host := n.hostname
msg, err := n.buildMessage(Data{entries, report, title, host})
msg, err := n.buildMessage(Data{n.data, entries, report})
if msg == "" {
// Log in go func in case we entered from Fire to avoid stalling
@ -240,10 +243,15 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
return
}
// StaticData is the part of the notification template data model set upon initialization
type StaticData struct {
Title string
Host string
}
// Data is the notification template data model
type Data struct {
StaticData
Entries []*log.Entry
Report t.Report
Title string
Host string
}

@ -49,11 +49,14 @@ var mockDataAllFresh = Data{
func mockDataFromStates(states ...s.State) Data {
hostname := "Mock"
prefix := ""
return Data{
Entries: legacyMockData.Entries,
Report: mocks.CreateMockProgressReport(states...),
Title: GetTitle(hostname),
Host: hostname,
StaticData: StaticData{
Title: GetTitle(hostname, prefix),
Host: hostname,
},
}
}
@ -77,7 +80,7 @@ var _ = Describe("Shoutrrr", func() {
cmd := new(cobra.Command)
flags.RegisterNotificationFlags(cmd)
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true)
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{})
entries := []*logrus.Entry{
{
@ -233,7 +236,7 @@ Turns out everything is on fire
When("batching notifications", func() {
When("no messages are queued", func() {
It("should not send any notification", func() {
shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://")
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), "logger://")
shoutrrr.StartNotification()
shoutrrr.SendNotification(nil)
Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))
@ -241,7 +244,7 @@ Turns out everything is on fire
})
When("at least one message is queued", func() {
It("should send a notification", func() {
shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://")
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), "logger://")
shoutrrr.StartNotification()
logrus.Info("This log message is sponsored by ContainrrrVPN")
shoutrrr.SendNotification(nil)
@ -250,6 +253,17 @@ Turns out everything is on fire
})
})
When("the title data field is empty", func() {
It("should not have set the title param", func() {
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{
Host: "test.host",
Title: "",
})
_, found := shoutrrr.params.Title()
Expect(found).ToNot(BeTrue())
})
})
When("sending notifications", func() {
It("SlowNotificationNotSent", func() {

Loading…
Cancel
Save