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
pull/1347/head
nils måsén 2 years ago committed by GitHub
parent a429c373ff
commit 7900471f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)
})
}

@ -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"
} }

@ -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
;; ;;

Loading…
Cancel
Save