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:
nils måsén 2021-06-27 09:05:01 +02:00 committed by GitHub
parent d0ecc23d72
commit e3dd8d688a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 853 additions and 598 deletions

View file

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

View file

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

View file

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

View 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")
}

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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