You've already forked watchtower
mirror of
https://github.com/containrrr/watchtower.git
synced 2025-07-12 23:50:20 +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:
19
cmd/root.go
19
cmd/root.go
@ -39,7 +39,6 @@ var (
|
|||||||
lifecycleHooks bool
|
lifecycleHooks bool
|
||||||
rollingRestart bool
|
rollingRestart bool
|
||||||
scope string
|
scope string
|
||||||
// Set on build using ldflags
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = NewRootCommand()
|
var rootCmd = NewRootCommand()
|
||||||
@ -75,6 +74,7 @@ func Execute() {
|
|||||||
// PreRun is a lifecycle hook that runs before the command is executed.
|
// PreRun is a lifecycle hook that runs before the command is executed.
|
||||||
func PreRun(cmd *cobra.Command, _ []string) {
|
func PreRun(cmd *cobra.Command, _ []string) {
|
||||||
f := cmd.PersistentFlags()
|
f := cmd.PersistentFlags()
|
||||||
|
flags.ProcessFlagAliases(f)
|
||||||
|
|
||||||
if enabled, _ := f.GetBool("no-color"); enabled {
|
if enabled, _ := f.GetBool("no-color"); enabled {
|
||||||
log.SetFormatter(&log.TextFormatter{
|
log.SetFormatter(&log.TextFormatter{
|
||||||
@ -94,18 +94,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||||||
log.SetLevel(log.TraceLevel)
|
log.SetLevel(log.TraceLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
pollingSet := f.Changed("interval")
|
scheduleSpec, _ = f.GetString("schedule")
|
||||||
schedule, _ := f.GetString("schedule")
|
|
||||||
cronLen := len(schedule)
|
|
||||||
|
|
||||||
if pollingSet && cronLen > 0 {
|
|
||||||
log.Fatal("Only schedule or interval can be defined, not both.")
|
|
||||||
} else if cronLen > 0 {
|
|
||||||
scheduleSpec, _ = f.GetString("schedule")
|
|
||||||
} else {
|
|
||||||
interval, _ := f.GetInt("interval")
|
|
||||||
scheduleSpec = "@every " + strconv.Itoa(interval) + "s"
|
|
||||||
}
|
|
||||||
|
|
||||||
flags.GetSecretsFromFiles(cmd)
|
flags.GetSecretsFromFiles(cmd)
|
||||||
cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
|
cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
|
||||||
@ -119,7 +108,9 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||||||
rollingRestart, _ = f.GetBool("rolling-restart")
|
rollingRestart, _ = f.GetBool("rolling-restart")
|
||||||
scope, _ = f.GetString("scope")
|
scope, _ = f.GetString("scope")
|
||||||
|
|
||||||
log.Debug(scope)
|
if scope != "" {
|
||||||
|
log.Debugf(`Using scope %q`, scope)
|
||||||
|
}
|
||||||
|
|
||||||
// configure environment vars for client
|
// configure environment vars for client
|
||||||
err := flags.EnvConfig(cmd)
|
err := flags.EnvConfig(cmd)
|
||||||
|
@ -3,6 +3,7 @@ package flags
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -153,22 +154,32 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
|||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
|
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
|
||||||
"Sets an authentication token to HTTP API requests.")
|
"Sets an authentication token to HTTP API requests.")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"http-api-periodic-polls",
|
"http-api-periodic-polls",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
|
viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
|
||||||
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
|
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
|
||||||
|
|
||||||
// https://no-color.org/
|
// https://no-color.org/
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"no-color",
|
"no-color",
|
||||||
"",
|
"",
|
||||||
viper.IsSet("NO_COLOR"),
|
viper.IsSet("NO_COLOR"),
|
||||||
"Disable ANSI color escape codes in log output")
|
"Disable ANSI color escape codes in log output")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"scope",
|
"scope",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_SCOPE"),
|
viper.GetString("WATCHTOWER_SCOPE"),
|
||||||
"Defines a monitoring scope for the Watchtower instance.")
|
"Defines a monitoring scope for the Watchtower instance.")
|
||||||
|
|
||||||
|
flags.StringP(
|
||||||
|
"porcelain",
|
||||||
|
"P",
|
||||||
|
viper.GetString("WATCHTOWER_PORCELAIN"),
|
||||||
|
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterNotificationFlags that are used by watchtower to send notifications
|
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||||
@ -343,6 +354,10 @@ Should only be used for testing.`)
|
|||||||
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
|
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
|
||||||
"When to warn about HEAD pull requests failing. Possible values: always, auto or never")
|
"When to warn about HEAD pull requests failing. Possible values: always, auto or never")
|
||||||
|
|
||||||
|
flags.Bool(
|
||||||
|
"notification-log-stdout",
|
||||||
|
viper.GetBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
|
||||||
|
"Write notification logs to stdout instead of logging (to stderr)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults provides default values for environment variables
|
// SetDefaults provides default values for environment variables
|
||||||
@ -504,3 +519,60 @@ func isFile(s string) bool {
|
|||||||
_, err := os.Stat(s)
|
_, err := os.Stat(s)
|
||||||
return !errors.Is(err, os.ErrNotExist)
|
return !errors.Is(err, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessFlagAliases updates the value of flags that are being set by helper flags
|
||||||
|
func ProcessFlagAliases(flags *pflag.FlagSet) {
|
||||||
|
|
||||||
|
porcelain, err := flags.GetString(`porcelain`)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(`Failed to get flag: %v`, err)
|
||||||
|
}
|
||||||
|
if porcelain != "" {
|
||||||
|
if porcelain != "v1" {
|
||||||
|
log.Fatalf(`Unknown porcelain version %q. Supported values: "v1"`, porcelain)
|
||||||
|
}
|
||||||
|
if err = appendFlagValue(flags, `notification-url`, `logger://`); err != nil {
|
||||||
|
log.Errorf(`Failed to set flag: %v`, err)
|
||||||
|
}
|
||||||
|
setFlagIfDefault(flags, `notification-log-stdout`, `true`)
|
||||||
|
setFlagIfDefault(flags, `notification-report`, `true`)
|
||||||
|
tpl := fmt.Sprintf(`porcelain.%s.summary-no-log`, porcelain)
|
||||||
|
setFlagIfDefault(flags, `notification-template`, tpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.Changed(`interval`) && flags.Changed(`schedule`) {
|
||||||
|
log.Fatal(`Only schedule or interval can be defined, not both.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update schedule flag to match interval if it's set, or to the default if none of them are
|
||||||
|
if flags.Changed(`interval`) || !flags.Changed(`schedule`) {
|
||||||
|
interval, _ := flags.GetInt(`interval`)
|
||||||
|
flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error {
|
||||||
|
flag := flags.Lookup(name)
|
||||||
|
if flag == nil {
|
||||||
|
return fmt.Errorf(`invalid flag name %q`, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
|
||||||
|
for _, value := range values {
|
||||||
|
flagValues.Append(value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFlagIfDefault(flags *pflag.FlagSet, name string, value string) {
|
||||||
|
if flags.Changed(name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := flags.Set(name, value); err != nil {
|
||||||
|
log.Errorf(`Failed to set flag: %v`, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -127,3 +128,71 @@ func TestIsFile(t *testing.T) {
|
|||||||
assert.False(t, isFile("https://google.com"), "an URL should never be considered a file")
|
assert.False(t, isFile("https://google.com"), "an URL should never be considered a file")
|
||||||
assert.True(t, isFile(os.Args[0]), "the currently running binary path should always be considered a file")
|
assert.True(t, isFile(os.Args[0]), "the currently running binary path should always be considered a file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadFlags(t *testing.T) {
|
||||||
|
logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessFlagAliases(t *testing.T) {
|
||||||
|
logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }
|
||||||
|
cmd := new(cobra.Command)
|
||||||
|
SetDefaults()
|
||||||
|
RegisterDockerFlags(cmd)
|
||||||
|
RegisterSystemFlags(cmd)
|
||||||
|
RegisterNotificationFlags(cmd)
|
||||||
|
|
||||||
|
require.NoError(t, cmd.ParseFlags([]string{
|
||||||
|
`--porcelain`, `v1`,
|
||||||
|
`--interval`, `10`,
|
||||||
|
}))
|
||||||
|
flags := cmd.Flags()
|
||||||
|
ProcessFlagAliases(flags)
|
||||||
|
|
||||||
|
urls, _ := flags.GetStringArray(`notification-url`)
|
||||||
|
assert.Contains(t, urls, `logger://`)
|
||||||
|
|
||||||
|
logStdout, _ := flags.GetBool(`notification-log-stdout`)
|
||||||
|
assert.True(t, logStdout)
|
||||||
|
|
||||||
|
report, _ := flags.GetBool(`notification-report`)
|
||||||
|
assert.True(t, report)
|
||||||
|
|
||||||
|
template, _ := flags.GetString(`notification-template`)
|
||||||
|
assert.Equal(t, `porcelain.v1.summary-no-log`, template)
|
||||||
|
|
||||||
|
sched, _ := flags.GetString(`schedule`)
|
||||||
|
assert.Equal(t, `@every 10s`, sched)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
||||||
|
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
||||||
|
cmd := new(cobra.Command)
|
||||||
|
SetDefaults()
|
||||||
|
RegisterDockerFlags(cmd)
|
||||||
|
RegisterSystemFlags(cmd)
|
||||||
|
RegisterNotificationFlags(cmd)
|
||||||
|
|
||||||
|
require.NoError(t, cmd.ParseFlags([]string{`--schedule`, `@now`, `--interval`, `10`}))
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
assert.PanicsWithValue(t, `FATAL`, func() {
|
||||||
|
ProcessFlagAliases(flags)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
|
||||||
|
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
||||||
|
cmd := new(cobra.Command)
|
||||||
|
SetDefaults()
|
||||||
|
RegisterDockerFlags(cmd)
|
||||||
|
RegisterSystemFlags(cmd)
|
||||||
|
RegisterNotificationFlags(cmd)
|
||||||
|
|
||||||
|
require.NoError(t, cmd.ParseFlags([]string{`--porcelain`, `cowboy`}))
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
assert.PanicsWithValue(t, `FATAL`, func() {
|
||||||
|
ProcessFlagAliases(flags)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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 {
|
type emailTypeNotifier struct {
|
||||||
url string
|
|
||||||
From, To string
|
From, To string
|
||||||
Server, User, Password, SubjectTag string
|
Server, User, Password, SubjectTag string
|
||||||
Port int
|
Port int
|
||||||
|
@ -21,20 +21,21 @@ func NewNotifier(c *cobra.Command) ty.Notifier {
|
|||||||
log.Fatalf("Notifications invalid log level: %s", err.Error())
|
log.Fatalf("Notifications invalid log level: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 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)
|
log.Fatalf("Unsupported notification log level provided: %s", level)
|
||||||
}
|
}
|
||||||
|
|
||||||
reportTemplate, _ := f.GetBool("notification-report")
|
reportTemplate, _ := f.GetBool("notification-report")
|
||||||
|
stdout, _ := f.GetBool("notification-log-stdout")
|
||||||
tplString, _ := f.GetString("notification-template")
|
tplString, _ := f.GetString("notification-template")
|
||||||
urls, _ := f.GetStringArray("notification-url")
|
urls, _ := f.GetStringArray("notification-url")
|
||||||
|
|
||||||
data := GetTemplateData(c)
|
data := GetTemplateData(c)
|
||||||
urls, delay := AppendLegacyUrls(urls, c, data.Title)
|
urls, delay := AppendLegacyUrls(urls, c, 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
|
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags
|
||||||
|
@ -3,6 +3,7 @@ package notifications
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
stdlog "log"
|
stdlog "log"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
@ -11,35 +12,14 @@ import (
|
|||||||
"github.com/containrrr/shoutrrr/pkg/types"
|
"github.com/containrrr/shoutrrr/pkg/types"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
log "github.com/sirupsen/logrus"
|
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
|
// LocalLog is a logrus logger that does not send entries as notifications
|
||||||
var LocalLog = log.WithField("notify", "no")
|
var LocalLog = log.WithField("notify", "no")
|
||||||
|
|
||||||
const (
|
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"
|
shoutrrrType = "shoutrrr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,9 +59,9 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
|
|||||||
return names
|
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)
|
log.AddHook(notifier)
|
||||||
|
|
||||||
// Do the sending in a separate goroutine so we don't block the main process.
|
// Do the sending in a separate goroutine so we don't block the main process.
|
||||||
@ -90,14 +70,19 @@ func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy
|
|||||||
return notifier
|
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)
|
tpl, err := getShoutrrrTemplate(tplString, legacy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Could not use configured notification template: %s. Using default template", err)
|
log.Errorf("Could not use configured notification template: %s. Using default template", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
traceWriter := log.StandardLogger().WriterLevel(log.TraceLevel)
|
var logger types.StdLogger
|
||||||
r, err := shoutrrr.NewSender(stdlog.New(traceWriter, "Shoutrrr: ", 0), urls...)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
|
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.
|
// Use fmt so it doesn't trigger another notification.
|
||||||
LocalLog.Info("Waiting for the notification goroutine to finish")
|
LocalLog.Info("Waiting for the notification goroutine to finish")
|
||||||
|
|
||||||
_ = <-n.done
|
<-n.done
|
||||||
}
|
}
|
||||||
|
|
||||||
// Levels return what log levels trigger notifications
|
// Levels return what log levels trigger notifications
|
||||||
@ -217,10 +202,15 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
|||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"ToUpper": strings.ToUpper,
|
"ToUpper": strings.ToUpper,
|
||||||
"ToLower": strings.ToLower,
|
"ToLower": strings.ToLower,
|
||||||
"Title": strings.Title,
|
"Title": cases.Title(language.AmericanEnglish).String,
|
||||||
}
|
}
|
||||||
tplBase := template.New("").Funcs(funcs)
|
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
|
// If we succeed in getting a non-empty template configuration
|
||||||
// try to parse the template string.
|
// try to parse the template string.
|
||||||
if tplString != "" {
|
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
|
// 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)
|
// template wasn't configured (the empty template string)
|
||||||
// fallback to using the default template.
|
// fallback to using the default template.
|
||||||
if err != nil || tplString == "" {
|
if err != nil || tplString == "" {
|
||||||
defaultTemplate := shoutrrrDefaultTemplate
|
defaultKey := `default`
|
||||||
if legacy {
|
if legacy {
|
||||||
defaultTemplate = shoutrrrDefaultLegacyTemplate
|
defaultKey = `default-legacy`
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl = template.Must(tplBase.Parse(defaultTemplate))
|
tpl = template.Must(tplBase.Parse(commonTemplates[defaultKey]))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
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("using legacy templates", func() {
|
||||||
|
|
||||||
When("no custom template is provided", func() {
|
When("no custom template is provided", func() {
|
||||||
@ -80,7 +90,7 @@ var _ = Describe("Shoutrrr", func() {
|
|||||||
cmd := new(cobra.Command)
|
cmd := new(cobra.Command)
|
||||||
flags.RegisterNotificationFlags(cmd)
|
flags.RegisterNotificationFlags(cmd)
|
||||||
|
|
||||||
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{})
|
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true, StaticData{}, false)
|
||||||
|
|
||||||
entries := []*logrus.Entry{
|
entries := []*logrus.Entry{
|
||||||
{
|
{
|
||||||
@ -168,7 +178,6 @@ var _ = Describe("Shoutrrr", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
When("using report templates", func() {
|
When("using report templates", func() {
|
||||||
|
|
||||||
When("no custom template is provided", func() {
|
When("no custom template is provided", func() {
|
||||||
It("should format the messages using the default template", func() {
|
It("should format the messages using the default template", func() {
|
||||||
expected := `4 Scanned, 2 Updated, 1 Failed
|
expected := `4 Scanned, 2 Updated, 1 Failed
|
||||||
@ -236,7 +245,7 @@ Turns out everything is on fire
|
|||||||
When("batching notifications", func() {
|
When("batching notifications", func() {
|
||||||
When("no messages are queued", func() {
|
When("no messages are queued", func() {
|
||||||
It("should not send any notification", func() {
|
It("should not send any notification", func() {
|
||||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), "logger://")
|
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), false, "logger://")
|
||||||
shoutrrr.StartNotification()
|
shoutrrr.StartNotification()
|
||||||
shoutrrr.SendNotification(nil)
|
shoutrrr.SendNotification(nil)
|
||||||
Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))
|
Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))
|
||||||
@ -244,7 +253,7 @@ Turns out everything is on fire
|
|||||||
})
|
})
|
||||||
When("at least one message is queued", func() {
|
When("at least one message is queued", func() {
|
||||||
It("should send a notification", func() {
|
It("should send a notification", func() {
|
||||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), "logger://")
|
shoutrrr := newShoutrrrNotifier("", allButTrace, true, StaticData{}, time.Duration(0), false, "logger://")
|
||||||
shoutrrr.StartNotification()
|
shoutrrr.StartNotification()
|
||||||
logrus.Info("This log message is sponsored by ContainrrrVPN")
|
logrus.Info("This log message is sponsored by ContainrrrVPN")
|
||||||
shoutrrr.SendNotification(nil)
|
shoutrrr.SendNotification(nil)
|
||||||
@ -258,7 +267,7 @@ Turns out everything is on fire
|
|||||||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{
|
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{
|
||||||
Host: "test.host",
|
Host: "test.host",
|
||||||
Title: "",
|
Title: "",
|
||||||
})
|
}, false)
|
||||||
_, found := shoutrrr.params.Title()
|
_, found := shoutrrr.params.Title()
|
||||||
Expect(found).ToNot(BeTrue())
|
Expect(found).ToNot(BeTrue())
|
||||||
})
|
})
|
||||||
@ -290,7 +299,7 @@ type blockingRouter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b blockingRouter) Send(_ string, _ *types.Params) []error {
|
func (b blockingRouter) Send(_ string, _ *types.Params) []error {
|
||||||
_ = <-b.unlock
|
<-b.unlock
|
||||||
b.sent <- true
|
b.sent <- true
|
||||||
return nil
|
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) {
|
func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||||
trimmedURL := strings.TrimRight(s.HookURL, "/")
|
trimmedURL := strings.TrimRight(s.HookURL, "/")
|
||||||
trimmedURL = strings.TrimLeft(trimmedURL, "https://")
|
trimmedURL = strings.TrimPrefix(trimmedURL, "https://")
|
||||||
parts := strings.Split(trimmedURL, "/")
|
parts := strings.Split(trimmedURL, "/")
|
||||||
|
|
||||||
if parts[0] == "discord.com" || parts[0] == "discordapp.com" {
|
if parts[0] == "discord.com" || parts[0] == "discordapp.com" {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type report struct {
|
type report struct {
|
||||||
@ -32,6 +33,33 @@ func (r *report) Stale() []types.ContainerReport {
|
|||||||
func (r *report) Fresh() []types.ContainerReport {
|
func (r *report) Fresh() []types.ContainerReport {
|
||||||
return r.fresh
|
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
|
// NewReport creates a types.Report from the supplied Progress
|
||||||
func NewReport(progress Progress) types.Report {
|
func NewReport(progress Progress) types.Report {
|
||||||
|
@ -8,6 +8,7 @@ type Report interface {
|
|||||||
Skipped() []ContainerReport
|
Skipped() []ContainerReport
|
||||||
Stale() []ContainerReport
|
Stale() []ContainerReport
|
||||||
Fresh() []ContainerReport
|
Fresh() []ContainerReport
|
||||||
|
All() []ContainerReport
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerReport represents a container that was included in watchtower session
|
// ContainerReport represents a container that was included in watchtower session
|
||||||
|
@ -122,4 +122,65 @@ function container-started() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
docker container inspect "$Name" | jq -r .[].State.StartedAt
|
docker container inspect "$Name" | jq -r .[].State.StartedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function container-exists() {
|
||||||
|
local Name=$1
|
||||||
|
if [ -z "$Name" ]; then
|
||||||
|
echo "NAME missing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker container inspect "$Name" 1> /dev/null 2> /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
function registry-exists() {
|
||||||
|
container-exists "$CONTAINER_PREFIX-registry"
|
||||||
|
}
|
||||||
|
|
||||||
|
function create-container() {
|
||||||
|
local container_name=$1
|
||||||
|
if [ -z "$container_name" ]; then
|
||||||
|
echo "NAME missing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local image_name="${2:-$container_name}"
|
||||||
|
|
||||||
|
echo -en "Creating \e[94m$container_name\e[0m container... "
|
||||||
|
local result
|
||||||
|
result=$(docker run -d --name "$container_name" "$(registry-host)/$image_name" 2>&1)
|
||||||
|
if [ "${#result}" -eq 64 ]; then
|
||||||
|
echo -e "\e[92m${result:0:12}\e[0m"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "\e[91mFailed!\n\e[97m$result\e[0m"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove-images() {
|
||||||
|
local image_name=$1
|
||||||
|
if [ -z "$image_name" ]; then
|
||||||
|
echo "NAME missing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local images
|
||||||
|
mapfile -t images < <(docker images -q "$image_name" | uniq)
|
||||||
|
if [ -n "${images[*]}" ]; then
|
||||||
|
docker image rm "${images[@]}"
|
||||||
|
else
|
||||||
|
echo "No images matched \"$image_name\""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove-repo-images() {
|
||||||
|
local image_name=$1
|
||||||
|
if [ -z "$image_name" ]; then
|
||||||
|
echo "NAME missing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
remove-images "$(registry-host)/images/$image_name"
|
||||||
}
|
}
|
27
scripts/du-cli.sh
Normal file → Executable file
27
scripts/du-cli.sh
Normal file → Executable file
@ -16,7 +16,7 @@ case $1 in
|
|||||||
registry-host
|
registry-host
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown keyword \"$2\""
|
echo "Unknown registry action \"$2\""
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
@ -28,8 +28,11 @@ case $1 in
|
|||||||
latest)
|
latest)
|
||||||
latest-image-rev "$3"
|
latest-image-rev "$3"
|
||||||
;;
|
;;
|
||||||
|
rm)
|
||||||
|
remove-repo-images "$3"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown keyword \"$2\""
|
echo "Unknown image action \"$2\""
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
@ -47,8 +50,26 @@ case $1 in
|
|||||||
started)
|
started)
|
||||||
container-started "$3"
|
container-started "$3"
|
||||||
;;
|
;;
|
||||||
|
create)
|
||||||
|
create-container "${@:3:2}"
|
||||||
|
;;
|
||||||
|
create-stale)
|
||||||
|
if [ -z "$3" ]; then
|
||||||
|
echo "NAME missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! registry-exists; then
|
||||||
|
echo "Registry container missing! Creating..."
|
||||||
|
start-registry || exit 1
|
||||||
|
fi
|
||||||
|
image_name="images/$3"
|
||||||
|
container_name=$3
|
||||||
|
$0 image rev "$image_name" || exit 1
|
||||||
|
$0 container create "$container_name" "$image_name" || exit 1
|
||||||
|
$0 image rev "$image_name" || exit 1
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown keyword \"$2\""
|
echo "Unknown container action \"$2\""
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
Reference in New Issue
Block a user