You've already forked watchtower
							
							
				mirror of
				https://github.com/containrrr/watchtower.git
				synced 2025-10-31 00:17:44 +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 | ||||
| 	rollingRestart bool | ||||
| 	scope          string | ||||
| 	// Set on build using ldflags | ||||
| ) | ||||
|  | ||||
| var rootCmd = NewRootCommand() | ||||
| @@ -75,6 +74,7 @@ func Execute() { | ||||
| // PreRun is a lifecycle hook that runs before the command is executed. | ||||
| func PreRun(cmd *cobra.Command, _ []string) { | ||||
| 	f := cmd.PersistentFlags() | ||||
| 	flags.ProcessFlagAliases(f) | ||||
|  | ||||
| 	if enabled, _ := f.GetBool("no-color"); enabled { | ||||
| 		log.SetFormatter(&log.TextFormatter{ | ||||
| @@ -94,18 +94,7 @@ func PreRun(cmd *cobra.Command, _ []string) { | ||||
| 		log.SetLevel(log.TraceLevel) | ||||
| 	} | ||||
|  | ||||
| 	pollingSet := f.Changed("interval") | ||||
| 	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" | ||||
| 	} | ||||
| 	scheduleSpec, _ = f.GetString("schedule") | ||||
|  | ||||
| 	flags.GetSecretsFromFiles(cmd) | ||||
| 	cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd) | ||||
| @@ -119,7 +108,9 @@ func PreRun(cmd *cobra.Command, _ []string) { | ||||
| 	rollingRestart, _ = f.GetBool("rolling-restart") | ||||
| 	scope, _ = f.GetString("scope") | ||||
|  | ||||
| 	log.Debug(scope) | ||||
| 	if scope != "" { | ||||
| 		log.Debugf(`Using scope %q`, scope) | ||||
| 	} | ||||
|  | ||||
| 	// configure environment vars for client | ||||
| 	err := flags.EnvConfig(cmd) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package flags | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| @@ -153,22 +154,32 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { | ||||
| 		"", | ||||
| 		viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), | ||||
| 		"Sets an authentication token to HTTP API requests.") | ||||
|  | ||||
| 	flags.BoolP( | ||||
| 		"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") | ||||
|  | ||||
| 	// https://no-color.org/ | ||||
| 	flags.BoolP( | ||||
| 		"no-color", | ||||
| 		"", | ||||
| 		viper.IsSet("NO_COLOR"), | ||||
| 		"Disable ANSI color escape codes in log output") | ||||
|  | ||||
| 	flags.StringP( | ||||
| 		"scope", | ||||
| 		"", | ||||
| 		viper.GetString("WATCHTOWER_SCOPE"), | ||||
| 		"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 | ||||
| @@ -343,6 +354,10 @@ Should only be used for testing.`) | ||||
| 		viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), | ||||
| 		"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 | ||||
| @@ -504,3 +519,60 @@ func isFile(s string) bool { | ||||
| 	_, err := os.Stat(s) | ||||
| 	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" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"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.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 { | ||||
| 	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 | ||||
|   | ||||
| @@ -122,4 +122,65 @@ function container-started() { | ||||
|     return 1 | ||||
|   fi | ||||
|   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 | ||||
|         ;; | ||||
|       *) | ||||
|         echo "Unknown keyword \"$2\"" | ||||
|         echo "Unknown registry action \"$2\"" | ||||
|         ;; | ||||
|     esac | ||||
|     ;; | ||||
| @@ -28,8 +28,11 @@ case $1 in | ||||
|       latest) | ||||
|         latest-image-rev "$3" | ||||
|         ;; | ||||
|       rm) | ||||
|         remove-repo-images "$3" | ||||
|         ;; | ||||
|       *) | ||||
|         echo "Unknown keyword \"$2\"" | ||||
|         echo "Unknown image action \"$2\"" | ||||
|         ;; | ||||
|     esac | ||||
|     ;; | ||||
| @@ -47,8 +50,26 @@ case $1 in | ||||
|       started) | ||||
|         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 | ||||
|     ;; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user