Skip to content

Commit 7900471

Browse files
authored
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
1 parent a429c37 commit 7900471

File tree

13 files changed

+344
-63
lines changed

13 files changed

+344
-63
lines changed

cmd/root.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ var (
3939
lifecycleHooks bool
4040
rollingRestart bool
4141
scope string
42-
// Set on build using ldflags
4342
)
4443

4544
var rootCmd = NewRootCommand()
@@ -75,6 +74,7 @@ func Execute() {
7574
// PreRun is a lifecycle hook that runs before the command is executed.
7675
func PreRun(cmd *cobra.Command, _ []string) {
7776
f := cmd.PersistentFlags()
77+
flags.ProcessFlagAliases(f)
7878

7979
if enabled, _ := f.GetBool("no-color"); enabled {
8080
log.SetFormatter(&log.TextFormatter{
@@ -94,18 +94,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
9494
log.SetLevel(log.TraceLevel)
9595
}
9696

97-
pollingSet := f.Changed("interval")
98-
schedule, _ := f.GetString("schedule")
99-
cronLen := len(schedule)
100-
101-
if pollingSet && cronLen > 0 {
102-
log.Fatal("Only schedule or interval can be defined, not both.")
103-
} else if cronLen > 0 {
104-
scheduleSpec, _ = f.GetString("schedule")
105-
} else {
106-
interval, _ := f.GetInt("interval")
107-
scheduleSpec = "@every " + strconv.Itoa(interval) + "s"
108-
}
97+
scheduleSpec, _ = f.GetString("schedule")
10998

11099
flags.GetSecretsFromFiles(cmd)
111100
cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
@@ -119,7 +108,9 @@ func PreRun(cmd *cobra.Command, _ []string) {
119108
rollingRestart, _ = f.GetBool("rolling-restart")
120109
scope, _ = f.GetString("scope")
121110

122-
log.Debug(scope)
111+
if scope != "" {
112+
log.Debugf(`Using scope %q`, scope)
113+
}
123114

124115
// configure environment vars for client
125116
err := flags.EnvConfig(cmd)

internal/flags/flags.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package flags
33
import (
44
"bufio"
55
"errors"
6+
"fmt"
67
"io/ioutil"
78
"os"
89
"strings"
@@ -153,22 +154,32 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
153154
"",
154155
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
155156
"Sets an authentication token to HTTP API requests.")
157+
156158
flags.BoolP(
157159
"http-api-periodic-polls",
158160
"",
159161
viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
160162
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
163+
161164
// https://no-color.org/
162165
flags.BoolP(
163166
"no-color",
164167
"",
165168
viper.IsSet("NO_COLOR"),
166169
"Disable ANSI color escape codes in log output")
170+
167171
flags.StringP(
168172
"scope",
169173
"",
170174
viper.GetString("WATCHTOWER_SCOPE"),
171175
"Defines a monitoring scope for the Watchtower instance.")
176+
177+
flags.StringP(
178+
"porcelain",
179+
"P",
180+
viper.GetString("WATCHTOWER_PORCELAIN"),
181+
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
182+
172183
}
173184

174185
// RegisterNotificationFlags that are used by watchtower to send notifications
@@ -343,6 +354,10 @@ Should only be used for testing.`)
343354
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
344355
"When to warn about HEAD pull requests failing. Possible values: always, auto or never")
345356

357+
flags.Bool(
358+
"notification-log-stdout",
359+
viper.GetBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
360+
"Write notification logs to stdout instead of logging (to stderr)")
346361
}
347362

348363
// SetDefaults provides default values for environment variables
@@ -504,3 +519,60 @@ func isFile(s string) bool {
504519
_, err := os.Stat(s)
505520
return !errors.Is(err, os.ErrNotExist)
506521
}
522+
523+
// ProcessFlagAliases updates the value of flags that are being set by helper flags
524+
func ProcessFlagAliases(flags *pflag.FlagSet) {
525+
526+
porcelain, err := flags.GetString(`porcelain`)
527+
if err != nil {
528+
log.Fatalf(`Failed to get flag: %v`, err)
529+
}
530+
if porcelain != "" {
531+
if porcelain != "v1" {
532+
log.Fatalf(`Unknown porcelain version %q. Supported values: "v1"`, porcelain)
533+
}
534+
if err = appendFlagValue(flags, `notification-url`, `logger://`); err != nil {
535+
log.Errorf(`Failed to set flag: %v`, err)
536+
}
537+
setFlagIfDefault(flags, `notification-log-stdout`, `true`)
538+
setFlagIfDefault(flags, `notification-report`, `true`)
539+
tpl := fmt.Sprintf(`porcelain.%s.summary-no-log`, porcelain)
540+
setFlagIfDefault(flags, `notification-template`, tpl)
541+
}
542+
543+
if flags.Changed(`interval`) && flags.Changed(`schedule`) {
544+
log.Fatal(`Only schedule or interval can be defined, not both.`)
545+
}
546+
547+
// update schedule flag to match interval if it's set, or to the default if none of them are
548+
if flags.Changed(`interval`) || !flags.Changed(`schedule`) {
549+
interval, _ := flags.GetInt(`interval`)
550+
flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
551+
}
552+
}
553+
554+
func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error {
555+
flag := flags.Lookup(name)
556+
if flag == nil {
557+
return fmt.Errorf(`invalid flag name %q`, name)
558+
}
559+
560+
if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
561+
for _, value := range values {
562+
flagValues.Append(value)
563+
}
564+
} else {
565+
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
566+
}
567+
568+
return nil
569+
}
570+
571+
func setFlagIfDefault(flags *pflag.FlagSet, name string, value string) {
572+
if flags.Changed(name) {
573+
return
574+
}
575+
if err := flags.Set(name, value); err != nil {
576+
log.Errorf(`Failed to set flag: %v`, err)
577+
}
578+
}

internal/flags/flags_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"testing"
77

8+
"github.com/sirupsen/logrus"
89
"github.com/spf13/cobra"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
@@ -127,3 +128,71 @@ func TestIsFile(t *testing.T) {
127128
assert.False(t, isFile("https://google.com"), "an URL should never be considered a file")
128129
assert.True(t, isFile(os.Args[0]), "the currently running binary path should always be considered a file")
129130
}
131+
132+
func TestReadFlags(t *testing.T) {
133+
logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }
134+
135+
}
136+
137+
func TestProcessFlagAliases(t *testing.T) {
138+
logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }
139+
cmd := new(cobra.Command)
140+
SetDefaults()
141+
RegisterDockerFlags(cmd)
142+
RegisterSystemFlags(cmd)
143+
RegisterNotificationFlags(cmd)
144+
145+
require.NoError(t, cmd.ParseFlags([]string{
146+
`--porcelain`, `v1`,
147+
`--interval`, `10`,
148+
}))
149+
flags := cmd.Flags()
150+
ProcessFlagAliases(flags)
151+
152+
urls, _ := flags.GetStringArray(`notification-url`)
153+
assert.Contains(t, urls, `logger://`)
154+
155+
logStdout, _ := flags.GetBool(`notification-log-stdout`)
156+
assert.True(t, logStdout)
157+
158+
report, _ := flags.GetBool(`notification-report`)
159+
assert.True(t, report)
160+
161+
template, _ := flags.GetString(`notification-template`)
162+
assert.Equal(t, `porcelain.v1.summary-no-log`, template)
163+
164+
sched, _ := flags.GetString(`schedule`)
165+
assert.Equal(t, `@every 10s`, sched)
166+
}
167+
168+
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
169+
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
170+
cmd := new(cobra.Command)
171+
SetDefaults()
172+
RegisterDockerFlags(cmd)
173+
RegisterSystemFlags(cmd)
174+
RegisterNotificationFlags(cmd)
175+
176+
require.NoError(t, cmd.ParseFlags([]string{`--schedule`, `@now`, `--interval`, `10`}))
177+
flags := cmd.Flags()
178+
179+
assert.PanicsWithValue(t, `FATAL`, func() {
180+
ProcessFlagAliases(flags)
181+
})
182+
}
183+
184+
func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
185+
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
186+
cmd := new(cobra.Command)
187+
SetDefaults()
188+
RegisterDockerFlags(cmd)
189+
RegisterSystemFlags(cmd)
190+
RegisterNotificationFlags(cmd)
191+
192+
require.NoError(t, cmd.ParseFlags([]string{`--porcelain`, `cowboy`}))
193+
flags := cmd.Flags()
194+
195+
assert.PanicsWithValue(t, `FATAL`, func() {
196+
ProcessFlagAliases(flags)
197+
})
198+
}

pkg/notifications/common_templates.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package notifications
2+
3+
var commonTemplates = map[string]string{
4+
`default-legacy`: "{{range .}}{{.Message}}{{println}}{{end}}",
5+
6+
`default`: `
7+
{{- if .Report -}}
8+
{{- with .Report -}}
9+
{{- if ( or .Updated .Failed ) -}}
10+
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
11+
{{- range .Updated}}
12+
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
13+
{{- end -}}
14+
{{- range .Fresh}}
15+
- {{.Name}} ({{.ImageName}}): {{.State}}
16+
{{- end -}}
17+
{{- range .Skipped}}
18+
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
19+
{{- end -}}
20+
{{- range .Failed}}
21+
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
22+
{{- end -}}
23+
{{- end -}}
24+
{{- end -}}
25+
{{- else -}}
26+
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
27+
{{- end -}}`,
28+
29+
`porcelain.v1.summary-no-log`: `
30+
{{- if .Report -}}
31+
{{- range .Report.All }}
32+
{{- .Name}} ({{.ImageName}}): {{.State -}}
33+
{{- with .Error}} Error: {{.}}{{end}}{{ println }}
34+
{{- else -}}
35+
no containers matched filter
36+
{{- end -}}
37+
{{- end -}}`,
38+
}
39+

pkg/notifications/email.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const (
1515
)
1616

1717
type emailTypeNotifier struct {
18-
url string
1918
From, To string
2019
Server, User, Password, SubjectTag string
2120
Port int

pkg/notifications/notifier.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@ func NewNotifier(c *cobra.Command) ty.Notifier {
2121
log.Fatalf("Notifications invalid log level: %s", err.Error())
2222
}
2323

24-
acceptedLogLevels := slackrus.LevelThreshold(logLevel)
24+
levels := slackrus.LevelThreshold(logLevel)
2525
// slackrus does not allow log level TRACE, even though it's an accepted log level for logrus
26-
if len(acceptedLogLevels) == 0 {
26+
if len(levels) == 0 {
2727
log.Fatalf("Unsupported notification log level provided: %s", level)
2828
}
2929

3030
reportTemplate, _ := f.GetBool("notification-report")
31+
stdout, _ := f.GetBool("notification-log-stdout")
3132
tplString, _ := f.GetString("notification-template")
3233
urls, _ := f.GetStringArray("notification-url")
3334

3435
data := GetTemplateData(c)
3536
urls, delay := AppendLegacyUrls(urls, c, data.Title)
3637

37-
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, data, delay, urls...)
38+
return newShoutrrrNotifier(tplString, levels, !reportTemplate, data, delay, stdout, urls...)
3839
}
3940

4041
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags

0 commit comments

Comments
 (0)