Skip to content

Commit

Permalink
feat: include additional info in startup (#809)
Browse files Browse the repository at this point in the history
  • Loading branch information
piksel authored Mar 28, 2021
1 parent 5e17ef6 commit 9fa2fd8
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 55 deletions.
5 changes: 5 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

VERSION=$(git describe)
echo "Building $VERSION..."
go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/cmd.version=$VERSION"
109 changes: 84 additions & 25 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package cmd

import (
metrics2 "github.com/containrrr/watchtower/pkg/metrics"
"math"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"

"github.com/containrrr/watchtower/pkg/api/metrics"
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
"github.com/containrrr/watchtower/pkg/api/update"

"github.com/containrrr/watchtower/internal/actions"
"github.com/containrrr/watchtower/internal/flags"
"github.com/containrrr/watchtower/pkg/api"
"github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/filters"
"github.com/containrrr/watchtower/pkg/metrics"
"github.com/containrrr/watchtower/pkg/notifications"
t "github.com/containrrr/watchtower/pkg/types"
"github.com/robfig/cron"
Expand All @@ -36,6 +38,8 @@ var (
lifecycleHooks bool
rollingRestart bool
scope string
// Set on build using ldflags
version = "v0.0.0-unknown"
)

var rootCmd = NewRootCommand()
Expand Down Expand Up @@ -69,7 +73,7 @@ func Execute() {
}

// PreRun is a lifecycle hook that runs before the command is executed.
func PreRun(cmd *cobra.Command, args []string) {
func PreRun(cmd *cobra.Command, _ []string) {
f := cmd.PersistentFlags()

if enabled, _ := f.GetBool("no-color"); enabled {
Expand Down Expand Up @@ -146,17 +150,15 @@ func PreRun(cmd *cobra.Command, args []string) {

// Run is the main execution flow of the command
func Run(c *cobra.Command, names []string) {
filter := filters.BuildFilter(names, enableLabel, scope)
filter, filterDesc := filters.BuildFilter(names, enableLabel, scope)
runOnce, _ := c.PersistentFlags().GetBool("run-once")
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")

apiToken, _ := c.PersistentFlags().GetString("http-api-token")

if runOnce {
if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {
log.Info("Running a one time update.")
}
writeStartupMessage(c, time.Time{}, filterDesc)
runUpdatesWithNotifications(filter)
notifier.Close()
os.Exit(0)
Expand All @@ -175,39 +177,99 @@ func Run(c *cobra.Command, names []string) {
}

if enableMetricsAPI {
metricsHandler := metrics.New()
metricsHandler := apiMetrics.New()
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
}

httpAPI.Start(enableUpdateAPI)
if err := httpAPI.Start(enableUpdateAPI); err != nil {
log.Error("failed to start API", err)
}

if err := runUpgradesOnSchedule(c, filter); err != nil {
if err := runUpgradesOnSchedule(c, filter, filterDesc); err != nil {
log.Error(err)
}

os.Exit(1)
}

func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error {
func formatDuration(d time.Duration) string {
sb := strings.Builder{}

hours := int64(d.Hours())
minutes := int64(math.Mod(d.Minutes(), 60))
seconds := int64(math.Mod(d.Seconds(), 60))

if hours == 1 {
sb.WriteString("1 hour")
} else if hours != 0 {
sb.WriteString(strconv.FormatInt(hours, 10))
sb.WriteString(" hours")
}

if hours != 0 && (seconds != 0 || minutes != 0) {
sb.WriteString(", ")
}

if minutes == 1 {
sb.WriteString("1 minute")
} else if minutes != 0 {
sb.WriteString(strconv.FormatInt(minutes, 10))
sb.WriteString(" minutes")
}

if minutes != 0 && (seconds != 0) {
sb.WriteString(", ")
}

if seconds == 1 {
sb.WriteString("1 second")
} else if seconds != 0 || (hours == 0 && minutes == 0) {
sb.WriteString(strconv.FormatInt(seconds, 10))
sb.WriteString(" seconds")
}

return sb.String()
}

func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {
schedMessage := "Running a one time update."
if !sched.IsZero() {
until := formatDuration(time.Until(sched))
schedMessage = "Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST") +
"\nNote that the first check will be performed in " + until
}

notifs := "Using no notifications"
notifList := notifier.String()
if len(notifList) > 0 {
notifs = "Using notifications: " + notifList
}

log.Info("Watchtower ", version, "\n", notifs, "\n", filtering, "\n", schedMessage)
}
}

func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string) error {
tryLockSem := make(chan bool, 1)
tryLockSem <- true

cron := cron.New()
err := cron.AddFunc(
scheduler := cron.New()
err := scheduler.AddFunc(
scheduleSpec,
func() {
select {
case v := <-tryLockSem:
defer func() { tryLockSem <- v }()
metric := runUpdatesWithNotifications(filter)
metrics2.RegisterScan(metric)
metrics.RegisterScan(metric)
default:
// Update was skipped
metrics2.RegisterScan(nil)
metrics.RegisterScan(nil)
log.Debug("Skipped another update already running.")
}

nextRuns := cron.Entries()
nextRuns := scheduler.Entries()
if len(nextRuns) > 0 {
log.Debug("Scheduled next run: " + nextRuns[0].Next.String())
}
Expand All @@ -217,26 +279,23 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error {
return err
}

if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {
log.Info("Starting Watchtower and scheduling first run: " + cron.Entries()[0].Schedule.Next(time.Now()).String())
}
writeStartupMessage(c, scheduler.Entries()[0].Schedule.Next(time.Now()), filtering)

cron.Start()
scheduler.Start()

// Graceful shut-down on SIGINT/SIGTERM
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
signal.Notify(interrupt, syscall.SIGTERM)

<-interrupt
cron.Stop()
scheduler.Stop()
log.Info("Waiting for running update to be finished...")
<-tryLockSem
return nil
}

func runUpdatesWithNotifications(filter t.Filter) *metrics2.Metric {

func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
notifier.StartNotification()
updateParams := t.UpdateParams{
Filter: filter,
Expand All @@ -247,10 +306,10 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics2.Metric {
LifecycleHooks: lifecycleHooks,
RollingRestart: rollingRestart,
}
metrics, err := actions.Update(client, updateParams)
metricResults, err := actions.Update(client, updateParams)
if err != nil {
log.Println(err)
}
notifier.SendNotification()
return metrics
return metricResults
}
35 changes: 32 additions & 3 deletions pkg/filters/filters.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package filters

import t "github.com/containrrr/watchtower/pkg/types"
import (
t "github.com/containrrr/watchtower/pkg/types"
"strings"
)

// WatchtowerContainersFilter filters only watchtower containers
func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }
Expand Down Expand Up @@ -68,19 +71,45 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter {
}

// BuildFilter creates the needed filter of containers
func BuildFilter(names []string, enableLabel bool, scope string) t.Filter {
func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) {
sb := strings.Builder{}
filter := NoFilter
filter = FilterByNames(names, filter)

if len(names) > 0 {
sb.WriteString("with name \"")
for i, n := range names {
sb.WriteString(n)
if i < len(names)-1 {
sb.WriteString(`" or "`)
}
}
sb.WriteString(`", `)
}

if enableLabel {
// If label filtering is enabled, containers should only be considered
// if the label is specifically set.
filter = FilterByEnableLabel(filter)
sb.WriteString("using enable label, ")
}
if scope != "" {
// If a scope has been defined, containers should only be considered
// if the scope is specifically set.
filter = FilterByScope(scope, filter)
sb.WriteString(`in scope "`)
sb.WriteString(scope)
sb.WriteString(`", `)
}
filter = FilterByDisabledLabel(filter)
return filter

filterDesc := "Checking all containers (except explicitly disabled with label)"
if sb.Len() > 0 {
filterDesc = "Only checking containers " + sb.String()

// Remove the last ", "
filterDesc = filterDesc[:len(filterDesc)-2]
}

return filter, filterDesc
}
6 changes: 4 additions & 2 deletions pkg/filters/filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ func TestBuildFilter(t *testing.T) {
var names []string
names = append(names, "test")

filter := BuildFilter(names, false, "")
filter, desc := BuildFilter(names, false, "")
assert.Contains(t, desc, "test")

container := new(mocks.FilterableContainer)
container.On("Name").Return("Invalid")
Expand Down Expand Up @@ -150,7 +151,8 @@ func TestBuildFilterEnableLabel(t *testing.T) {
var names []string
names = append(names, "test")

filter := BuildFilter(names, true, "")
filter, desc := BuildFilter(names, true, "")
assert.Contains(t, desc, "using enable label")

container := new(mocks.FilterableContainer)
container.On("Enabled").Return(false, false)
Expand Down
21 changes: 21 additions & 0 deletions pkg/notifications/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
"strings"
)

// Notifier can send log output as notification to admins, with optional batching.
Expand Down Expand Up @@ -42,6 +43,26 @@ func NewNotifier(c *cobra.Command) *Notifier {
return n
}

func (n *Notifier) String() string {
if len(n.types) < 1 {
return ""
}

sb := strings.Builder{}
for _, notif := range n.types {
for _, name := range notif.GetNames() {
sb.WriteString(name)
sb.WriteString(", ")
}
}
names := sb.String()

// remove the last separator
names = names[:len(names)-2]

return names
}

// getNotificationTypes produces an array of notifiers from a list of types
func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier {
output := make([]ty.Notifier, 0)
Expand Down
Loading

0 comments on commit 9fa2fd8

Please sign in to comment.