mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-21 21:30:48 +02:00
feat: add porcelain output (#1337)
* feat: add porcaline output * feat(du-cli): add create-stale action add create-stale action Signed-off-by: nils måsén * test(flags): add alias tests * fix stray format string ref * fix shell liniting problems * feat(du-cli): remove created images * add test for common template * fix interval/schedule logic * use porcelain arg as template version * fix editor save artifacts * use simpler v1 template Signed-off-by: nils måsén
This commit is contained in:
parent
a429c373ff
commit
7900471f88
13 changed files with 344 additions and 63 deletions
39
pkg/notifications/common_templates.go
Normal file
39
pkg/notifications/common_templates.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package notifications
|
||||
|
||||
var commonTemplates = map[string]string{
|
||||
`default-legacy`: "{{range .}}{{.Message}}{{println}}{{end}}",
|
||||
|
||||
`default`: `
|
||||
{{- 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 -}}`,
|
||||
|
||||
`porcelain.v1.summary-no-log`: `
|
||||
{{- if .Report -}}
|
||||
{{- range .Report.All }}
|
||||
{{- .Name}} ({{.ImageName}}): {{.State -}}
|
||||
{{- with .Error}} Error: {{.}}{{end}}{{ println }}
|
||||
{{- else -}}
|
||||
no containers matched filter
|
||||
{{- end -}}
|
||||
{{- end -}}`,
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ const (
|
|||
)
|
||||
|
||||
type emailTypeNotifier struct {
|
||||
url string
|
||||
From, To string
|
||||
Server, User, Password, SubjectTag string
|
||||
Port int
|
||||
|
|
|
@ -21,20 +21,21 @@ func NewNotifier(c *cobra.Command) ty.Notifier {
|
|||
log.Fatalf("Notifications invalid log level: %s", err.Error())
|
||||
}
|
||||
|
||||
acceptedLogLevels := slackrus.LevelThreshold(logLevel)
|
||||
levels := slackrus.LevelThreshold(logLevel)
|
||||
// slackrus does not allow log level TRACE, even though it's an accepted log level for logrus
|
||||
if len(acceptedLogLevels) == 0 {
|
||||
if len(levels) == 0 {
|
||||
log.Fatalf("Unsupported notification log level provided: %s", level)
|
||||
}
|
||||
|
||||
reportTemplate, _ := f.GetBool("notification-report")
|
||||
stdout, _ := f.GetBool("notification-log-stdout")
|
||||
tplString, _ := f.GetString("notification-template")
|
||||
urls, _ := f.GetStringArray("notification-url")
|
||||
|
||||
data := GetTemplateData(c)
|
||||
urls, delay := AppendLegacyUrls(urls, c, data.Title)
|
||||
|
||||
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, data, delay, urls...)
|
||||
return newShoutrrrNotifier(tplString, levels, !reportTemplate, data, delay, stdout, urls...)
|
||||
}
|
||||
|
||||
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags
|
||||
|
|
|
@ -3,6 +3,7 @@ package notifications
|
|||
import (
|
||||
"bytes"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
@ -11,35 +12,14 @@ import (
|
|||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// LocalLog is a logrus logger that does not send entries as notifications
|
||||
var LocalLog = log.WithField("notify", "no")
|
||||
|
||||
const (
|
||||
shoutrrrDefaultLegacyTemplate = "{{range .}}{{.Message}}{{println}}{{end}}"
|
||||
shoutrrrDefaultTemplate = `
|
||||
{{- 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 -}}`
|
||||
shoutrrrType = "shoutrrr"
|
||||
)
|
||||
|
||||
|
@ -79,9 +59,9 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
|
|||
return names
|
||||
}
|
||||
|
||||
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, data StaticData, delay time.Duration, urls ...string) t.Notifier {
|
||||
func newShoutrrrNotifier(tplString string, levels []log.Level, legacy bool, data StaticData, delay time.Duration, stdout bool, urls ...string) t.Notifier {
|
||||
|
||||
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy, data)
|
||||
notifier := createNotifier(urls, levels, tplString, legacy, data, stdout)
|
||||
log.AddHook(notifier)
|
||||
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
|
@ -90,14 +70,19 @@ func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy
|
|||
return notifier
|
||||
}
|
||||
|
||||
func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool, data StaticData) *shoutrrrTypeNotifier {
|
||||
func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool, data StaticData, stdout 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...)
|
||||
var logger types.StdLogger
|
||||
if stdout {
|
||||
logger = stdlog.New(os.Stdout, ``, 0)
|
||||
} else {
|
||||
logger = stdlog.New(log.StandardLogger().WriterLevel(log.TraceLevel), "Shoutrrr: ", 0)
|
||||
}
|
||||
r, err := shoutrrr.NewSender(logger, urls...)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
|
||||
}
|
||||
|
@ -190,7 +175,7 @@ func (n *shoutrrrTypeNotifier) Close() {
|
|||
// Use fmt so it doesn't trigger another notification.
|
||||
LocalLog.Info("Waiting for the notification goroutine to finish")
|
||||
|
||||
_ = <-n.done
|
||||
<-n.done
|
||||
}
|
||||
|
||||
// Levels return what log levels trigger notifications
|
||||
|
@ -217,10 +202,15 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
|||
funcs := template.FuncMap{
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"Title": strings.Title,
|
||||
"Title": cases.Title(language.AmericanEnglish).String,
|
||||
}
|
||||
tplBase := template.New("").Funcs(funcs)
|
||||
|
||||
if builtin, found := commonTemplates[tplString]; found {
|
||||
log.WithField(`template`, tplString).Debug(`Using common template`)
|
||||
tplString = builtin
|
||||
}
|
||||
|
||||
// If we succeed in getting a non-empty template configuration
|
||||
// try to parse the template string.
|
||||
if tplString != "" {
|
||||
|
@ -228,16 +218,16 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
|||
}
|
||||
|
||||
// If we had an error (either from parsing the template string
|
||||
// or from getting the template configuration) or we a
|
||||
// or from getting the template configuration) or a
|
||||
// template wasn't configured (the empty template string)
|
||||
// fallback to using the default template.
|
||||
if err != nil || tplString == "" {
|
||||
defaultTemplate := shoutrrrDefaultTemplate
|
||||
defaultKey := `default`
|
||||
if legacy {
|
||||
defaultTemplate = shoutrrrDefaultLegacyTemplate
|
||||
defaultKey = `default-legacy`
|
||||
}
|
||||
|
||||
tpl = template.Must(tplBase.Parse(defaultTemplate))
|
||||
tpl = template.Must(tplBase.Parse(commonTemplates[defaultKey]))
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
@ -73,6 +73,16 @@ var _ = Describe("Shoutrrr", func() {
|
|||
})
|
||||
})
|
||||
|
||||
When("passing a common template name", func() {
|
||||
It("should format using that template", func() {
|
||||
expected := `
|
||||
updt1 (mock/updt1:latest): Updated
|
||||
`[1:]
|
||||
data := mockDataFromStates(s.UpdatedState)
|
||||
Expect(getTemplatedResult(`porcelain.v1.summary-no-log`, false, data)).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
When("using legacy templates", func() {
|
||||
|
||||
When("no custom template is provided", func() {
|
||||
|
@ -80,7 +90,7 @@ var _ = Describe("Shoutrrr", func() {
|
|||
cmd := new(cobra.Command)
|
||||
flags.RegisterNotificationFlags(cmd)
|
||||
|
||||
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{})
|
||||
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{}, false)
|
||||
|
||||
entries := []*logrus.Entry{
|
||||
{
|
||||
|
@ -168,7 +178,6 @@ var _ = Describe("Shoutrrr", func() {
|
|||
})
|
||||
|
||||
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
|
||||
|
@ -236,7 +245,7 @@ Turns out everything is on fire
|
|||
When("batching notifications", func() {
|
||||
When("no messages are queued", func() {
|
||||
It("should not send any notification", func() {
|
||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), "logger://")
|
||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), false, "logger://")
|
||||
shoutrrr.StartNotification()
|
||||
shoutrrr.SendNotification(nil)
|
||||
Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))
|
||||
|
@ -244,7 +253,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, StaticData{}, time.Duration(0), "logger://")
|
||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), false, "logger://")
|
||||
shoutrrr.StartNotification()
|
||||
logrus.Info("This log message is sponsored by ContainrrrVPN")
|
||||
shoutrrr.SendNotification(nil)
|
||||
|
@ -258,7 +267,7 @@ Turns out everything is on fire
|
|||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{
|
||||
Host: "test.host",
|
||||
Title: "",
|
||||
})
|
||||
}, false)
|
||||
_, found := shoutrrr.params.Title()
|
||||
Expect(found).ToNot(BeTrue())
|
||||
})
|
||||
|
@ -290,7 +299,7 @@ type blockingRouter struct {
|
|||
}
|
||||
|
||||
func (b blockingRouter) Send(_ string, _ *types.Params) []error {
|
||||
_ = <-b.unlock
|
||||
<-b.unlock
|
||||
b.sent <- true
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
|
|||
|
||||
func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
trimmedURL := strings.TrimRight(s.HookURL, "/")
|
||||
trimmedURL = strings.TrimLeft(trimmedURL, "https://")
|
||||
trimmedURL = strings.TrimPrefix(trimmedURL, "https://")
|
||||
parts := strings.Split(trimmedURL, "/")
|
||||
|
||||
if parts[0] == "discord.com" || parts[0] == "discordapp.com" {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"sort"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
type report struct {
|
||||
|
@ -32,6 +33,33 @@ func (r *report) Stale() []types.ContainerReport {
|
|||
func (r *report) Fresh() []types.ContainerReport {
|
||||
return r.fresh
|
||||
}
|
||||
func (r *report) All() []types.ContainerReport {
|
||||
allLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh)
|
||||
all := make([]types.ContainerReport, 0, allLen)
|
||||
|
||||
presentIds := map[types.ContainerID][]string{}
|
||||
|
||||
appendUnique := func(reports []types.ContainerReport) {
|
||||
for _, cr := range reports {
|
||||
if _, found := presentIds[cr.ID()]; found {
|
||||
continue
|
||||
}
|
||||
all = append(all, cr)
|
||||
presentIds[cr.ID()] = nil
|
||||
}
|
||||
}
|
||||
|
||||
appendUnique(r.updated)
|
||||
appendUnique(r.failed)
|
||||
appendUnique(r.skipped)
|
||||
appendUnique(r.stale)
|
||||
appendUnique(r.fresh)
|
||||
appendUnique(r.scanned)
|
||||
|
||||
sort.Sort(sortableContainers(all))
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// NewReport creates a types.Report from the supplied Progress
|
||||
func NewReport(progress Progress) types.Report {
|
||||
|
|
|
@ -8,6 +8,7 @@ type Report interface {
|
|||
Skipped() []ContainerReport
|
||||
Stale() []ContainerReport
|
||||
Fresh() []ContainerReport
|
||||
All() []ContainerReport
|
||||
}
|
||||
|
||||
// ContainerReport represents a container that was included in watchtower session
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue