Skip to content

Commit

Permalink
feat: add porcelain output (#1337)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
piksel committed Aug 14, 2022
1 parent a429c37 commit 7900471
Show file tree
Hide file tree
Showing 13 changed files with 344 additions and 63 deletions.
19 changes: 5 additions & 14 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ var (
lifecycleHooks bool
rollingRestart bool
scope string
// Set on build using ldflags
)

var rootCmd = NewRootCommand()
Expand Down Expand Up @@ -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{
Expand All @@ -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)
Expand All @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package flags
import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
69 changes: 69 additions & 0 deletions internal/flags/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 changes: 39 additions & 0 deletions pkg/notifications/common_templates.go
Original file line number Diff line number Diff line change
@@ -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 -}}`,
}

1 change: 0 additions & 1 deletion pkg/notifications/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const (
)

type emailTypeNotifier struct {
url string
From, To string
Server, User, Password, SubjectTag string
Port int
Expand Down
7 changes: 4 additions & 3 deletions pkg/notifications/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7900471

Please sign in to comment.