mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-21 21:30:48 +02:00
Session report collection and report templates (#981)
* wip: notification stats * make report notifications optional * linting/documentation fixes * linting/documentation fixes * merge types.Container and container.Interface * smaller naming/format fixes * use typed image/container IDs * simplify notifier and update tests * add missed doc comments * lint fixes * remove unused constructors * rename old/new current/latest
This commit is contained in:
parent
d0ecc23d72
commit
e3dd8d688a
32 changed files with 853 additions and 598 deletions
|
@ -25,11 +25,6 @@ type emailTypeNotifier struct {
|
|||
delay time.Duration
|
||||
}
|
||||
|
||||
// NewEmailNotifier is a factory method creating a new email notifier instance
|
||||
func NewEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
return newEmailNotifier(c, acceptedLogLevels)
|
||||
}
|
||||
|
||||
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
|
|
|
@ -22,11 +22,6 @@ type gotifyTypeNotifier struct {
|
|||
logLevels []log.Level
|
||||
}
|
||||
|
||||
// NewGotifyNotifier is a factory method creating a new gotify notifier instance
|
||||
func NewGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier {
|
||||
return newGotifyNotifier(c, levels)
|
||||
}
|
||||
|
||||
func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
|
|
|
@ -18,11 +18,6 @@ type msTeamsTypeNotifier struct {
|
|||
data bool
|
||||
}
|
||||
|
||||
// NewMsTeamsNotifier is a factory method creating a new teams notifier instance
|
||||
func NewMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
return newMsTeamsNotifier(cmd, acceptedLogLevels)
|
||||
}
|
||||
|
||||
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
|
||||
flags := cmd.PersistentFlags()
|
||||
|
|
13
pkg/notifications/notifications_suite_test.go
Normal file
13
pkg/notifications/notifications_suite_test.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package notifications_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestNotifications(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Notifications Suite")
|
||||
}
|
|
@ -6,18 +6,10 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Notifier can send log output as notification to admins, with optional batching.
|
||||
type Notifier struct {
|
||||
types []ty.Notifier
|
||||
}
|
||||
|
||||
// NewNotifier creates and returns a new Notifier, using global configuration.
|
||||
func NewNotifier(c *cobra.Command) *Notifier {
|
||||
n := &Notifier{}
|
||||
|
||||
func NewNotifier(c *cobra.Command) ty.Notifier {
|
||||
f := c.PersistentFlags()
|
||||
|
||||
level, _ := f.GetString("notifications-level")
|
||||
|
@ -32,54 +24,26 @@ func NewNotifier(c *cobra.Command) *Notifier {
|
|||
log.Fatalf("Unsupported notification log level provided: %s", level)
|
||||
}
|
||||
|
||||
reportTemplate, _ := f.GetBool("notification-report")
|
||||
tplString, _ := f.GetString("notification-template")
|
||||
urls, _ := f.GetStringArray("notification-url")
|
||||
|
||||
urls = AppendLegacyUrls(urls, c)
|
||||
|
||||
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, urls...)
|
||||
}
|
||||
|
||||
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags
|
||||
func AppendLegacyUrls(urls []string, cmd *cobra.Command) []string {
|
||||
|
||||
// Parse types and create notifiers.
|
||||
types, err := f.GetStringSlice("notifications")
|
||||
types, err := cmd.Flags().GetStringSlice("notifications")
|
||||
if err != nil {
|
||||
log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal()
|
||||
log.WithError(err).Fatal("could not read notifications argument")
|
||||
}
|
||||
|
||||
n.types = n.getNotificationTypes(c, acceptedLogLevels, types)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *Notifier) String() string {
|
||||
if len(n.types) < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
for _, notif := range n.types {
|
||||
for _, name := range notif.GetNames() {
|
||||
sb.WriteString(name)
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
}
|
||||
|
||||
if sb.Len() < 2 {
|
||||
// No notification services are configured, return early as the separator strip is not applicable
|
||||
return "none"
|
||||
}
|
||||
|
||||
names := sb.String()
|
||||
|
||||
// remove the last separator
|
||||
names = names[:len(names)-2]
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// getNotificationTypes produces an array of notifiers from a list of types
|
||||
func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier {
|
||||
output := make([]ty.Notifier, 0)
|
||||
|
||||
for _, t := range types {
|
||||
|
||||
if t == shoutrrrType {
|
||||
output = append(output, newShoutrrrNotifier(cmd, levels))
|
||||
continue
|
||||
}
|
||||
|
||||
var legacyNotifier ty.ConvertibleNotifier
|
||||
var err error
|
||||
|
||||
|
@ -89,9 +53,11 @@ func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level,
|
|||
case slackType:
|
||||
legacyNotifier = newSlackNotifier(cmd, []log.Level{})
|
||||
case msTeamsType:
|
||||
legacyNotifier = newMsTeamsNotifier(cmd, levels)
|
||||
legacyNotifier = newMsTeamsNotifier(cmd, []log.Level{})
|
||||
case gotifyType:
|
||||
legacyNotifier = newGotifyNotifier(cmd, []log.Level{})
|
||||
case shoutrrrType:
|
||||
continue
|
||||
default:
|
||||
log.Fatalf("Unknown notification type %q", t)
|
||||
// Not really needed, used for nil checking static analysis
|
||||
|
@ -102,40 +68,11 @@ func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level,
|
|||
if err != nil {
|
||||
log.Fatal("failed to create notification config:", err)
|
||||
}
|
||||
urls = append(urls, shoutrrrURL)
|
||||
|
||||
log.WithField("URL", shoutrrrURL).Trace("created Shoutrrr URL from legacy notifier")
|
||||
|
||||
notifier := newShoutrrrNotifierFromURL(
|
||||
cmd,
|
||||
shoutrrrURL,
|
||||
levels,
|
||||
)
|
||||
|
||||
output = append(output, notifier)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// StartNotification starts a log batch. Notifications will be accumulated after this point and only sent when SendNotification() is called.
|
||||
func (n *Notifier) StartNotification() {
|
||||
for _, t := range n.types {
|
||||
t.StartNotification()
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotification sends any notifications accumulated since StartNotification() was called.
|
||||
func (n *Notifier) SendNotification() {
|
||||
for _, t := range n.types {
|
||||
t.SendNotification()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes all notifiers.
|
||||
func (n *Notifier) Close() {
|
||||
for _, t := range n.types {
|
||||
t.Close()
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
// GetTitle returns a common notification title with hostname appended
|
||||
|
|
|
@ -4,24 +4,14 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/containrrr/watchtower/cmd"
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
"github.com/containrrr/watchtower/pkg/notifications"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestActions(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Notifier Suite")
|
||||
}
|
||||
|
||||
var _ = Describe("notifications", func() {
|
||||
Describe("the notifier", func() {
|
||||
When("only empty notifier types are provided", func() {
|
||||
|
@ -36,11 +26,11 @@ var _ = Describe("notifications", func() {
|
|||
Expect(err).NotTo(HaveOccurred())
|
||||
notif := notifications.NewNotifier(command)
|
||||
|
||||
Expect(notif.String()).To(Equal("none"))
|
||||
Expect(notif.GetNames()).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
Describe("the slack notifier", func() {
|
||||
builderFn := notifications.NewSlackNotifier
|
||||
// builderFn := notifications.NewSlackNotifier
|
||||
|
||||
When("passing a discord url to the slack notifier", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
|
@ -62,11 +52,11 @@ var _ = Describe("notifications", func() {
|
|||
|
||||
It("should return a discord url when using a hook url with the domain discord.com", func() {
|
||||
hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token)
|
||||
testURL(builderFn, buildArgs(hookURL), expected)
|
||||
testURL(buildArgs(hookURL), expected)
|
||||
})
|
||||
It("should return a discord url when using a hook url with the domain discordapp.com", func() {
|
||||
hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discordapp.com", channel, token)
|
||||
testURL(builderFn, buildArgs(hookURL), expected)
|
||||
testURL(buildArgs(hookURL), expected)
|
||||
})
|
||||
})
|
||||
When("converting a slack service config into a shoutrrr url", func() {
|
||||
|
@ -86,21 +76,21 @@ var _ = Describe("notifications", func() {
|
|||
expectedOutput := fmt.Sprintf("slack://%s@%s/%s/%s?color=%s&title=%s", username, tokenA, tokenB, tokenC, color, title)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
"slack",
|
||||
"--notification-slack-hook-url",
|
||||
hookURL,
|
||||
"--notification-slack-identifier",
|
||||
username,
|
||||
}
|
||||
|
||||
testURL(builderFn, args, expectedOutput)
|
||||
testURL(args, expectedOutput)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("the gotify notifier", func() {
|
||||
When("converting a gotify service config into a shoutrrr url", func() {
|
||||
builderFn := notifications.NewGotifyNotifier
|
||||
|
||||
It("should return the expected URL", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
@ -112,21 +102,21 @@ var _ = Describe("notifications", func() {
|
|||
expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
"gotify",
|
||||
"--notification-gotify-url",
|
||||
fmt.Sprintf("https://%s", host),
|
||||
"--notification-gotify-token",
|
||||
token,
|
||||
}
|
||||
|
||||
testURL(builderFn, args, expectedOutput)
|
||||
testURL(args, expectedOutput)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("the teams notifier", func() {
|
||||
When("converting a teams service config into a shoutrrr url", func() {
|
||||
builderFn := notifications.NewMsTeamsNotifier
|
||||
|
||||
It("should return the expected URL", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
@ -141,24 +131,25 @@ var _ = Describe("notifications", func() {
|
|||
expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
"msteams",
|
||||
"--notification-msteams-hook",
|
||||
hookURL,
|
||||
}
|
||||
|
||||
testURL(builderFn, args, expectedOutput)
|
||||
testURL(args, expectedOutput)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("the email notifier", func() {
|
||||
|
||||
builderFn := notifications.NewEmailNotifier
|
||||
|
||||
When("converting an email service config into a shoutrrr url", func() {
|
||||
It("should set the from address in the URL", func() {
|
||||
fromAddress := "lala@example.com"
|
||||
expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, "mail@example.com", "Plain")
|
||||
args := []string{
|
||||
"--notifications",
|
||||
"email",
|
||||
"--notification-email-from",
|
||||
fromAddress,
|
||||
"--notification-email-to",
|
||||
|
@ -170,7 +161,7 @@ var _ = Describe("notifications", func() {
|
|||
"--notification-email-server",
|
||||
"mail.containrrr.dev",
|
||||
}
|
||||
testURL(builderFn, args, expectedOutput)
|
||||
testURL(args, expectedOutput)
|
||||
})
|
||||
|
||||
It("should return the expected URL", func() {
|
||||
|
@ -180,6 +171,8 @@ var _ = Describe("notifications", func() {
|
|||
expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, toAddress, "Plain")
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
"email",
|
||||
"--notification-email-from",
|
||||
fromAddress,
|
||||
"--notification-email-to",
|
||||
|
@ -192,7 +185,7 @@ var _ = Describe("notifications", func() {
|
|||
"mail.containrrr.dev",
|
||||
}
|
||||
|
||||
testURL(builderFn, args, expectedOutput)
|
||||
testURL(args, expectedOutput)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -214,9 +207,7 @@ func buildExpectedURL(username string, password string, host string, port int, f
|
|||
url.QueryEscape(to))
|
||||
}
|
||||
|
||||
type builderFn = func(c *cobra.Command, acceptedLogLevels []log.Level) types.ConvertibleNotifier
|
||||
|
||||
func testURL(builder builderFn, args []string, expectedURL string) {
|
||||
func testURL(args []string, expectedURL string) {
|
||||
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
@ -224,10 +215,9 @@ func testURL(builder builderFn, args []string, expectedURL string) {
|
|||
err := command.ParseFlags(args)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
notifier := builder(command, []log.Level{})
|
||||
actualURL, err := notifier.GetURL(command)
|
||||
urls := notifications.AppendLegacyUrls([]string{}, command)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(actualURL).To(Equal(expectedURL))
|
||||
Expect(urls).To(ContainElement(expectedURL))
|
||||
}
|
||||
|
|
|
@ -11,12 +11,26 @@ import (
|
|||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
shoutrrrDefaultTemplate = "{{range .}}{{.Message}}{{println}}{{end}}"
|
||||
shoutrrrType = "shoutrrr"
|
||||
shoutrrrDefaultLegacyTemplate = "{{range .}}{{.Message}}{{println}}{{end}}"
|
||||
shoutrrrDefaultTemplate = `{{- 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 -}}`
|
||||
shoutrrrType = "shoutrrr"
|
||||
)
|
||||
|
||||
type router interface {
|
||||
|
@ -25,41 +39,49 @@ type router interface {
|
|||
|
||||
// Implements Notifier, logrus.Hook
|
||||
type shoutrrrTypeNotifier struct {
|
||||
Urls []string
|
||||
Router router
|
||||
entries []*log.Entry
|
||||
logLevels []log.Level
|
||||
template *template.Template
|
||||
messages chan string
|
||||
done chan bool
|
||||
Urls []string
|
||||
Router router
|
||||
entries []*log.Entry
|
||||
logLevels []log.Level
|
||||
template *template.Template
|
||||
messages chan string
|
||||
done chan bool
|
||||
legacyTemplate bool
|
||||
}
|
||||
|
||||
// GetScheme returns the scheme part of a Shoutrrr URL
|
||||
func GetScheme(url string) string {
|
||||
schemeEnd := strings.Index(url, ":")
|
||||
if schemeEnd <= 0 {
|
||||
return "invalid"
|
||||
}
|
||||
return url[:schemeEnd]
|
||||
}
|
||||
|
||||
func (n *shoutrrrTypeNotifier) GetNames() []string {
|
||||
names := make([]string, len(n.Urls))
|
||||
for i, u := range n.Urls {
|
||||
schemeEnd := strings.Index(u, ":")
|
||||
if schemeEnd <= 0 {
|
||||
names[i] = "invalid"
|
||||
continue
|
||||
}
|
||||
names[i] = u[:schemeEnd]
|
||||
names[i] = GetScheme(u)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
||||
flags := c.PersistentFlags()
|
||||
urls, _ := flags.GetStringArray("notification-url")
|
||||
tpl := getShoutrrrTemplate(c)
|
||||
return createSender(urls, acceptedLogLevels, tpl)
|
||||
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, urls ...string) t.Notifier {
|
||||
|
||||
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy)
|
||||
log.AddHook(notifier)
|
||||
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
go sendNotifications(notifier)
|
||||
|
||||
return notifier
|
||||
}
|
||||
|
||||
func newShoutrrrNotifierFromURL(c *cobra.Command, url string, levels []log.Level) t.Notifier {
|
||||
tpl := getShoutrrrTemplate(c)
|
||||
return createSender([]string{url}, levels, tpl)
|
||||
}
|
||||
|
||||
func createSender(urls []string, levels []log.Level, template *template.Template) t.Notifier {
|
||||
func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool) *shoutrrrTypeNotifier {
|
||||
tpl, err := getShoutrrrTemplate(tplString, legacy)
|
||||
if err != nil {
|
||||
log.Errorf("Could not use configured notification template: %s. Using default template", err)
|
||||
}
|
||||
|
||||
traceWriter := log.StandardLogger().WriterLevel(log.TraceLevel)
|
||||
r, err := shoutrrr.NewSender(stdlog.New(traceWriter, "Shoutrrr: ", 0), urls...)
|
||||
|
@ -67,21 +89,15 @@ func createSender(urls []string, levels []log.Level, template *template.Template
|
|||
log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
|
||||
}
|
||||
|
||||
n := &shoutrrrTypeNotifier{
|
||||
Urls: urls,
|
||||
Router: r,
|
||||
messages: make(chan string, 1),
|
||||
done: make(chan bool),
|
||||
logLevels: levels,
|
||||
template: template,
|
||||
return &shoutrrrTypeNotifier{
|
||||
Urls: urls,
|
||||
Router: r,
|
||||
messages: make(chan string, 1),
|
||||
done: make(chan bool),
|
||||
logLevels: levels,
|
||||
template: tpl,
|
||||
legacyTemplate: legacy,
|
||||
}
|
||||
|
||||
log.AddHook(n)
|
||||
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
go sendNotifications(n)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func sendNotifications(n *shoutrrrTypeNotifier) {
|
||||
|
@ -90,8 +106,9 @@ func sendNotifications(n *shoutrrrTypeNotifier) {
|
|||
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
scheme := GetScheme(n.Urls[i])
|
||||
// Use fmt so it doesn't trigger another notification.
|
||||
fmt.Println("Failed to send notification via shoutrrr (url="+n.Urls[i]+"): ", err)
|
||||
fmt.Printf("Failed to send shoutrrr notification (#%d, %s): %v\n", i, scheme, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,17 +116,21 @@ func sendNotifications(n *shoutrrrTypeNotifier) {
|
|||
n.done <- true
|
||||
}
|
||||
|
||||
func (n *shoutrrrTypeNotifier) buildMessage(entries []*log.Entry) string {
|
||||
func (n *shoutrrrTypeNotifier) buildMessage(data Data) string {
|
||||
var body bytes.Buffer
|
||||
if err := n.template.Execute(&body, entries); err != nil {
|
||||
var templateData interface{} = data
|
||||
if n.legacyTemplate {
|
||||
templateData = data.Entries
|
||||
}
|
||||
if err := n.template.Execute(&body, templateData); err != nil {
|
||||
fmt.Printf("Failed to execute Shoutrrrr template: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return body.String()
|
||||
}
|
||||
|
||||
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry) {
|
||||
msg := n.buildMessage(entries)
|
||||
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) {
|
||||
msg := n.buildMessage(Data{entries, report})
|
||||
n.messages <- msg
|
||||
}
|
||||
|
||||
|
@ -119,12 +140,12 @@ func (n *shoutrrrTypeNotifier) StartNotification() {
|
|||
}
|
||||
}
|
||||
|
||||
func (n *shoutrrrTypeNotifier) SendNotification() {
|
||||
if n.entries == nil || len(n.entries) <= 0 {
|
||||
return
|
||||
}
|
||||
func (n *shoutrrrTypeNotifier) SendNotification(report t.Report) {
|
||||
//if n.entries == nil || len(n.entries) <= 0 {
|
||||
// return
|
||||
//}
|
||||
|
||||
n.sendEntries(n.entries)
|
||||
n.sendEntries(n.entries, report)
|
||||
n.entries = nil
|
||||
}
|
||||
|
||||
|
@ -146,36 +167,23 @@ func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {
|
|||
n.entries = append(n.entries, entry)
|
||||
} else {
|
||||
// Log output generated outside a cycle is sent immediately.
|
||||
n.sendEntries([]*log.Entry{entry})
|
||||
n.sendEntries([]*log.Entry{entry}, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getShoutrrrTemplate(c *cobra.Command) *template.Template {
|
||||
var tpl *template.Template
|
||||
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
tplString, err := flags.GetString("notification-template")
|
||||
|
||||
func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) {
|
||||
funcs := template.FuncMap{
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"Title": strings.Title,
|
||||
}
|
||||
tplBase := template.New("").Funcs(funcs)
|
||||
|
||||
// If we succeed in getting a non-empty template configuration
|
||||
// try to parse the template string.
|
||||
if tplString != "" && err == nil {
|
||||
tpl, err = template.New("").Funcs(funcs).Parse(tplString)
|
||||
}
|
||||
|
||||
// In case of errors (either from parsing the template string
|
||||
// or from getting the template configuration) log an error
|
||||
// message about this and the fact that we'll use the default
|
||||
// template instead.
|
||||
if err != nil {
|
||||
log.Errorf("Could not use configured notification template: %s. Using default template", err)
|
||||
if tplString != "" {
|
||||
tpl, err = tplBase.Parse(tplString)
|
||||
}
|
||||
|
||||
// If we had an error (either from parsing the template string
|
||||
|
@ -183,8 +191,19 @@ func getShoutrrrTemplate(c *cobra.Command) *template.Template {
|
|||
// template wasn't configured (the empty template string)
|
||||
// fallback to using the default template.
|
||||
if err != nil || tplString == "" {
|
||||
tpl = template.Must(template.New("").Funcs(funcs).Parse(shoutrrrDefaultTemplate))
|
||||
defaultTemplate := shoutrrrDefaultTemplate
|
||||
if legacy {
|
||||
defaultTemplate = shoutrrrDefaultLegacyTemplate
|
||||
}
|
||||
|
||||
tpl = template.Must(tplBase.Parse(defaultTemplate))
|
||||
}
|
||||
|
||||
return tpl
|
||||
return
|
||||
}
|
||||
|
||||
// Data is the notification template data model
|
||||
type Data struct {
|
||||
Entries []*log.Entry
|
||||
Report t.Report
|
||||
}
|
||||
|
|
|
@ -2,169 +2,226 @@ package notifications
|
|||
|
||||
import (
|
||||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
log "github.com/sirupsen/logrus"
|
||||
s "github.com/containrrr/watchtower/pkg/session"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShoutrrrDefaultTemplate(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
shoutrrr := &shoutrrrTypeNotifier{
|
||||
template: getShoutrrrTemplate(cmd),
|
||||
}
|
||||
|
||||
entries := []*log.Entry{
|
||||
var legacyMockData = Data{
|
||||
Entries: []*logrus.Entry{
|
||||
{
|
||||
Message: "foo bar",
|
||||
},
|
||||
}
|
||||
|
||||
s := shoutrrr.buildMessage(entries)
|
||||
|
||||
require.Equal(t, "foo bar\n", s)
|
||||
}
|
||||
|
||||
func TestShoutrrrTemplate(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
flags.RegisterNotificationFlags(cmd)
|
||||
err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}"})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
shoutrrr := &shoutrrrTypeNotifier{
|
||||
template: getShoutrrrTemplate(cmd),
|
||||
}
|
||||
|
||||
entries := []*log.Entry{
|
||||
{
|
||||
Level: log.InfoLevel,
|
||||
Message: "foo bar",
|
||||
},
|
||||
}
|
||||
|
||||
s := shoutrrr.buildMessage(entries)
|
||||
|
||||
require.Equal(t, "info: foo bar\n", s)
|
||||
}
|
||||
|
||||
func TestShoutrrrStringFunctions(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
flags.RegisterNotificationFlags(cmd)
|
||||
err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level | printf \"%v\" | ToUpper }}: {{.Message | ToLower }} {{.Message | Title }}{{println}}{{end}}"})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
shoutrrr := &shoutrrrTypeNotifier{
|
||||
template: getShoutrrrTemplate(cmd),
|
||||
}
|
||||
|
||||
entries := []*log.Entry{
|
||||
{
|
||||
Level: log.InfoLevel,
|
||||
Level: logrus.InfoLevel,
|
||||
Message: "foo Bar",
|
||||
},
|
||||
}
|
||||
|
||||
s := shoutrrr.buildMessage(entries)
|
||||
|
||||
require.Equal(t, "INFO: foo bar Foo Bar\n", s)
|
||||
},
|
||||
}
|
||||
|
||||
func TestShoutrrrInvalidTemplateUsesTemplate(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
flags.RegisterNotificationFlags(cmd)
|
||||
err := cmd.ParseFlags([]string{"--notification-template={{"})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
shoutrrr := &shoutrrrTypeNotifier{
|
||||
template: getShoutrrrTemplate(cmd),
|
||||
func mockDataFromStates(states ...s.State) Data {
|
||||
return Data{
|
||||
Entries: legacyMockData.Entries,
|
||||
Report: mocks.CreateMockProgressReport(states...),
|
||||
}
|
||||
|
||||
shoutrrrDefault := &shoutrrrTypeNotifier{
|
||||
template: template.Must(template.New("").Parse(shoutrrrDefaultTemplate)),
|
||||
}
|
||||
|
||||
entries := []*log.Entry{
|
||||
{
|
||||
Message: "foo bar",
|
||||
},
|
||||
}
|
||||
|
||||
s := shoutrrr.buildMessage(entries)
|
||||
sd := shoutrrrDefault.buildMessage(entries)
|
||||
|
||||
require.Equal(t, sd, s)
|
||||
}
|
||||
|
||||
var _ = Describe("Shoutrrr", func() {
|
||||
var logBuffer *gbytes.Buffer
|
||||
|
||||
BeforeEach(func() {
|
||||
logBuffer = gbytes.NewBuffer()
|
||||
logrus.SetOutput(logBuffer)
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableColors: true,
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
})
|
||||
|
||||
When("using legacy templates", func() {
|
||||
|
||||
When("no custom template is provided", func() {
|
||||
It("should format the messages using the default template", func() {
|
||||
cmd := new(cobra.Command)
|
||||
flags.RegisterNotificationFlags(cmd)
|
||||
|
||||
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true)
|
||||
|
||||
entries := []*logrus.Entry{
|
||||
{
|
||||
Message: "foo bar",
|
||||
},
|
||||
}
|
||||
|
||||
s := shoutrrr.buildMessage(Data{Entries: entries})
|
||||
|
||||
Expect(s).To(Equal("foo bar\n"))
|
||||
})
|
||||
})
|
||||
When("given a valid custom template", func() {
|
||||
It("should format the messages using the custom template", func() {
|
||||
|
||||
tplString := `{{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}`
|
||||
tpl, err := getShoutrrrTemplate(tplString, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
shoutrrr := &shoutrrrTypeNotifier{
|
||||
template: tpl,
|
||||
legacyTemplate: true,
|
||||
}
|
||||
|
||||
entries := []*logrus.Entry{
|
||||
{
|
||||
Level: logrus.InfoLevel,
|
||||
Message: "foo bar",
|
||||
},
|
||||
}
|
||||
|
||||
s := shoutrrr.buildMessage(Data{Entries: entries})
|
||||
|
||||
Expect(s).To(Equal("info: foo bar\n"))
|
||||
})
|
||||
})
|
||||
|
||||
When("given an invalid custom template", func() {
|
||||
It("should format the messages using the default template", func() {
|
||||
invNotif, err := createNotifierWithTemplate(`{{ intentionalSyntaxError`, true)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
defNotif, err := createNotifierWithTemplate(``, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(invNotif.buildMessage(legacyMockData)).To(Equal(defNotif.buildMessage(legacyMockData)))
|
||||
})
|
||||
})
|
||||
|
||||
When("given a template that is using ToUpper function", func() {
|
||||
It("should return the text in UPPER CASE", func() {
|
||||
tplString := `{{range .}}{{ .Message | ToUpper }}{{end}}`
|
||||
Expect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal("FOO BAR"))
|
||||
})
|
||||
})
|
||||
|
||||
When("given a template that is using ToLower function", func() {
|
||||
It("should return the text in lower case", func() {
|
||||
tplString := `{{range .}}{{ .Message | ToLower }}{{end}}`
|
||||
Expect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal("foo bar"))
|
||||
})
|
||||
})
|
||||
|
||||
When("given a template that is using Title function", func() {
|
||||
It("should return the text in Title Case", func() {
|
||||
tplString := `{{range .}}{{ .Message | Title }}{{end}}`
|
||||
Expect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal("Foo Bar"))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
When("using report templates", func() {
|
||||
|
||||
When("no custom template is provided", func() {
|
||||
It("should format the messages using the default template", func() {
|
||||
expected := `4 Scanned, 2 Updated, 1 Failed
|
||||
- updt1 (mock/updt1:latest): 01d110000000 updated to d0a110000000
|
||||
- updt2 (mock/updt2:latest): 01d120000000 updated to d0a120000000
|
||||
- frsh1 (mock/frsh1:latest): Fresh
|
||||
- skip1 (mock/skip1:latest): Skipped: unpossible
|
||||
- fail1 (mock/fail1:latest): Failed: accidentally the whole container
|
||||
`
|
||||
data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState)
|
||||
Expect(getTemplatedResult(``, false, data)).To(Equal(expected))
|
||||
})
|
||||
|
||||
It("should format the messages using the default template", func() {
|
||||
expected := `1 Scanned, 0 Updated, 0 Failed
|
||||
- frsh1 (mock/frsh1:latest): Fresh
|
||||
`
|
||||
data := mockDataFromStates(s.FreshState)
|
||||
Expect(getTemplatedResult(``, false, data)).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("sending notifications", func() {
|
||||
|
||||
It("SlowNotificationNotSent", func() {
|
||||
_, blockingRouter := sendNotificationsWithBlockingRouter(true)
|
||||
|
||||
Eventually(blockingRouter.sent).Should(Not(Receive()))
|
||||
|
||||
})
|
||||
|
||||
It("SlowNotificationSent", func() {
|
||||
shoutrrr, blockingRouter := sendNotificationsWithBlockingRouter(true)
|
||||
|
||||
blockingRouter.unlock <- true
|
||||
shoutrrr.Close()
|
||||
|
||||
Eventually(blockingRouter.sent).Should(Receive(BeTrue()))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
type blockingRouter struct {
|
||||
unlock chan bool
|
||||
sent chan bool
|
||||
}
|
||||
|
||||
func (b blockingRouter) Send(message string, params *types.Params) []error {
|
||||
func (b blockingRouter) Send(_ string, _ *types.Params) []error {
|
||||
_ = <-b.unlock
|
||||
b.sent <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSlowNotificationNotSent(t *testing.T) {
|
||||
_, blockingRouter := sendNotificationsWithBlockingRouter()
|
||||
|
||||
notifSent := false
|
||||
select {
|
||||
case notifSent = <-blockingRouter.sent:
|
||||
default:
|
||||
}
|
||||
|
||||
require.Equal(t, false, notifSent)
|
||||
}
|
||||
|
||||
func TestSlowNotificationSent(t *testing.T) {
|
||||
shoutrrr, blockingRouter := sendNotificationsWithBlockingRouter()
|
||||
|
||||
blockingRouter.unlock <- true
|
||||
shoutrrr.Close()
|
||||
|
||||
notifSent := false
|
||||
select {
|
||||
case notifSent = <-blockingRouter.sent:
|
||||
default:
|
||||
}
|
||||
require.Equal(t, true, notifSent)
|
||||
}
|
||||
|
||||
func sendNotificationsWithBlockingRouter() (*shoutrrrTypeNotifier, *blockingRouter) {
|
||||
cmd := new(cobra.Command)
|
||||
func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *blockingRouter) {
|
||||
|
||||
router := &blockingRouter{
|
||||
unlock: make(chan bool, 1),
|
||||
sent: make(chan bool, 1),
|
||||
}
|
||||
|
||||
tpl, err := getShoutrrrTemplate("", legacy)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
shoutrrr := &shoutrrrTypeNotifier{
|
||||
template: getShoutrrrTemplate(cmd),
|
||||
messages: make(chan string, 1),
|
||||
done: make(chan bool),
|
||||
Router: router,
|
||||
template: tpl,
|
||||
messages: make(chan string, 1),
|
||||
done: make(chan bool),
|
||||
Router: router,
|
||||
legacyTemplate: legacy,
|
||||
}
|
||||
|
||||
entry := &log.Entry{
|
||||
entry := &logrus.Entry{
|
||||
Message: "foo bar",
|
||||
}
|
||||
|
||||
go sendNotifications(shoutrrr)
|
||||
|
||||
shoutrrr.StartNotification()
|
||||
shoutrrr.Fire(entry)
|
||||
_ = shoutrrr.Fire(entry)
|
||||
|
||||
shoutrrr.SendNotification()
|
||||
shoutrrr.SendNotification(nil)
|
||||
|
||||
return shoutrrr, router
|
||||
}
|
||||
|
||||
func createNotifierWithTemplate(tplString string, legacy bool) (*shoutrrrTypeNotifier, error) {
|
||||
tpl, err := getShoutrrrTemplate(tplString, legacy)
|
||||
|
||||
return &shoutrrrTypeNotifier{
|
||||
template: tpl,
|
||||
legacyTemplate: legacy,
|
||||
}, err
|
||||
}
|
||||
|
||||
func getTemplatedResult(tplString string, legacy bool, data Data) (string, error) {
|
||||
notifier, err := createNotifierWithTemplate(tplString, legacy)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return notifier.buildMessage(data), err
|
||||
}
|
||||
|
|
|
@ -19,11 +19,6 @@ type slackTypeNotifier struct {
|
|||
slackrus.SlackrusHook
|
||||
}
|
||||
|
||||
// NewSlackNotifier is a factory function used to generate new instance of the slack notifier type
|
||||
func NewSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
return newSlackNotifier(c, acceptedLogLevels)
|
||||
}
|
||||
|
||||
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
// Package notifications ...
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license.
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// SendMail connects to the server at addr, switches to TLS if
|
||||
// possible, authenticates with the optional mechanism a if possible,
|
||||
// and then sends an email from address from, to addresses to, with
|
||||
// message msg.
|
||||
// The addr must include a port, as in "mail.example.com:smtp".
|
||||
//
|
||||
// The addresses in the to parameter are the SMTP RCPT addresses.
|
||||
//
|
||||
// The msg parameter should be an RFC 822-style email with headers
|
||||
// first, a blank line, and then the message body. The lines of msg
|
||||
// should be CRLF terminated. The msg headers should usually include
|
||||
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
|
||||
// messages is accomplished by including an email address in the to
|
||||
// parameter but not including it in the msg headers.
|
||||
//
|
||||
// The SendMail function and the net/smtp package are low-level
|
||||
// mechanisms and provide no support for DKIM signing, MIME
|
||||
// attachments (see the mime/multipart package), or other mail
|
||||
// functionality. Higher-level packages exist outside of the standard
|
||||
// library.
|
||||
func SendMail(addr string, insecureSkipVerify bool, a smtp.Auth, from string, to []string, msg []byte) error {
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.Hello("localHost"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
serverName, _, _ := net.SplitHostPort(addr)
|
||||
config := &tls.Config{ServerName: serverName, InsecureSkipVerify: insecureSkipVerify}
|
||||
if err = c.StartTLS(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package notifications
|
||||
|
||||
import "bytes"
|
||||
|
||||
// SplitSubN splits a string into a list of string with each having
|
||||
// a maximum number of characters n
|
||||
func SplitSubN(s string, n int) []string {
|
||||
sub := ""
|
||||
subs := []string{}
|
||||
|
||||
runes := bytes.Runes([]byte(s))
|
||||
l := len(runes)
|
||||
for i, r := range runes {
|
||||
sub = sub + string(r)
|
||||
if (i+1)%n == 0 {
|
||||
subs = append(subs, sub)
|
||||
sub = ""
|
||||
} else if (i + 1) == l {
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
}
|
||||
|
||||
return subs
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue