mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-16 15:10:12 +01:00
feat(api): implement new api handler
This commit is contained in:
parent
72e437f173
commit
47091761a5
17 changed files with 571 additions and 294 deletions
249
cmd/root.go
249
cmd/root.go
|
|
@ -1,22 +1,19 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions"
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"github.com/containrrr/watchtower/pkg/api"
|
||||
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
|
||||
"github.com/containrrr/watchtower/pkg/api/update"
|
||||
"github.com/containrrr/watchtower/pkg/api/updates"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/filters"
|
||||
"github.com/containrrr/watchtower/pkg/metrics"
|
||||
|
|
@ -31,20 +28,16 @@ import (
|
|||
var (
|
||||
client container.Client
|
||||
scheduleSpec string
|
||||
cleanup bool
|
||||
noRestart bool
|
||||
monitorOnly bool
|
||||
enableLabel bool
|
||||
disableContainers []string
|
||||
notifier t.Notifier
|
||||
timeout time.Duration
|
||||
lifecycleHooks bool
|
||||
rollingRestart bool
|
||||
scope string
|
||||
labelPrecedence bool
|
||||
|
||||
up = t.UpdateParams{}
|
||||
)
|
||||
|
||||
var rootCmd = NewRootCommand()
|
||||
var localLog = notifications.LocalLog
|
||||
|
||||
// NewRootCommand creates the root command for watchtower
|
||||
func NewRootCommand() *cobra.Command {
|
||||
|
|
@ -87,18 +80,18 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
scheduleSpec, _ = f.GetString("schedule")
|
||||
|
||||
flags.GetSecretsFromFiles(cmd)
|
||||
cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
|
||||
up.Cleanup, up.NoRestart, up.MonitorOnly, up.Timeout = flags.ReadFlags(cmd)
|
||||
|
||||
if timeout < 0 {
|
||||
if up.Timeout < 0 {
|
||||
log.Fatal("Please specify a positive value for timeout value.")
|
||||
}
|
||||
|
||||
enableLabel, _ = f.GetBool("label-enable")
|
||||
disableContainers, _ = f.GetStringSlice("disable-containers")
|
||||
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
||||
rollingRestart, _ = f.GetBool("rolling-restart")
|
||||
up.LifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
||||
up.RollingRestart, _ = f.GetBool("rolling-restart")
|
||||
scope, _ = f.GetString("scope")
|
||||
labelPrecedence, _ = f.GetBool("label-take-precedence")
|
||||
up.LabelPrecedence, _ = f.GetBool("label-take-precedence")
|
||||
|
||||
if scope != "" {
|
||||
log.Debugf(`Using scope %q`, scope)
|
||||
|
|
@ -110,25 +103,22 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
noPull, _ := f.GetBool("no-pull")
|
||||
includeStopped, _ := f.GetBool("include-stopped")
|
||||
includeRestarting, _ := f.GetBool("include-restarting")
|
||||
reviveStopped, _ := f.GetBool("revive-stopped")
|
||||
removeVolumes, _ := f.GetBool("remove-volumes")
|
||||
warnOnHeadPullFailed, _ := f.GetString("warn-on-head-failure")
|
||||
var clientOpts = container.ClientOptions{}
|
||||
|
||||
if monitorOnly && noPull {
|
||||
noPull, _ := f.GetBool("no-pull")
|
||||
clientOpts.PullImages = !noPull
|
||||
clientOpts.IncludeStopped, _ = f.GetBool("include-stopped")
|
||||
clientOpts.IncludeRestarting, _ = f.GetBool("include-restarting")
|
||||
clientOpts.ReviveStopped, _ = f.GetBool("revive-stopped")
|
||||
clientOpts.RemoveVolumes, _ = f.GetBool("remove-volumes")
|
||||
warnOnHeadPullFailed, _ := f.GetString("warn-on-head-failure")
|
||||
clientOpts.WarnOnHeadFailed = container.WarningStrategy(warnOnHeadPullFailed)
|
||||
|
||||
if up.MonitorOnly && noPull {
|
||||
log.Warn("Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.")
|
||||
}
|
||||
|
||||
client = container.NewClient(container.ClientOptions{
|
||||
PullImages: !noPull,
|
||||
IncludeStopped: includeStopped,
|
||||
ReviveStopped: reviveStopped,
|
||||
RemoveVolumes: removeVolumes,
|
||||
IncludeRestarting: includeRestarting,
|
||||
WarnOnHeadFailed: container.WarningStrategy(warnOnHeadPullFailed),
|
||||
})
|
||||
client = container.NewClient(clientOpts)
|
||||
|
||||
notifier = notifications.NewNotifier(cmd)
|
||||
notifier.AddLogHook()
|
||||
|
|
@ -137,13 +127,16 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
// Run is the main execution flow of the command
|
||||
func Run(c *cobra.Command, names []string) {
|
||||
filter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope)
|
||||
up.Filter = filter
|
||||
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-updates")
|
||||
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
|
||||
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
|
||||
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
||||
healthCheck, _ := c.PersistentFlags().GetBool("health-check")
|
||||
|
||||
enableScheduler := !enableUpdateAPI || unblockHTTPAPI
|
||||
|
||||
if healthCheck {
|
||||
// health check should not have pid 1
|
||||
if os.Getpid() == 1 {
|
||||
|
|
@ -153,61 +146,97 @@ func Run(c *cobra.Command, names []string) {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
if rollingRestart && monitorOnly {
|
||||
if up.RollingRestart && up.MonitorOnly {
|
||||
log.Fatal("Rolling restarts is not compatible with the global monitor only flag")
|
||||
}
|
||||
|
||||
awaitDockerClient()
|
||||
|
||||
if err := actions.CheckForSanity(client, filter, rollingRestart); err != nil {
|
||||
if err := actions.CheckForSanity(client, up.Filter, up.RollingRestart); err != nil {
|
||||
logNotifyExit(err)
|
||||
}
|
||||
|
||||
if runOnce {
|
||||
writeStartupMessage(c, time.Time{}, filterDesc)
|
||||
runUpdatesWithNotifications(filter)
|
||||
runUpdatesWithNotifications(up)
|
||||
notifier.Close()
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil {
|
||||
if err := actions.CheckForMultipleWatchtowerInstances(client, up.Cleanup, scope); err != nil {
|
||||
logNotifyExit(err)
|
||||
}
|
||||
|
||||
// The lock is shared between the scheduler and the HTTP API. It only allows one update to run at a time.
|
||||
updateLock := make(chan bool, 1)
|
||||
updateLock <- true
|
||||
// The lock is shared between the scheduler and the HTTP API. It only allows one updates to run at a time.
|
||||
updateLock := sync.Mutex{}
|
||||
|
||||
httpAPI := api.New(apiToken)
|
||||
|
||||
if enableUpdateAPI {
|
||||
updateHandler := update.New(func(images []string) {
|
||||
metric := runUpdatesWithNotifications(filters.FilterByImage(images, filter))
|
||||
metrics.RegisterScan(metric)
|
||||
}, updateLock)
|
||||
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
|
||||
// If polling isn't enabled the scheduler is never started and
|
||||
// we need to trigger the startup messages manually.
|
||||
if !unblockHTTPAPI {
|
||||
writeStartupMessage(c, time.Time{}, filterDesc)
|
||||
}
|
||||
httpAPI.EnableUpdates(func(paramsFunc updates.ModifyParamsFunc) t.Report {
|
||||
apiUpdateParams := up
|
||||
paramsFunc(&apiUpdateParams)
|
||||
if up.MonitorOnly && !apiUpdateParams.MonitorOnly {
|
||||
apiUpdateParams.MonitorOnly = true
|
||||
localLog.Warn("Ignoring request to disable monitor only through API")
|
||||
}
|
||||
report := runUpdatesWithNotifications(apiUpdateParams)
|
||||
metrics.RegisterScan(metrics.NewMetric(report))
|
||||
return report
|
||||
}, &updateLock)
|
||||
}
|
||||
|
||||
if enableMetricsAPI {
|
||||
metricsHandler := apiMetrics.New()
|
||||
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
|
||||
httpAPI.EnableMetrics()
|
||||
}
|
||||
|
||||
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
if err := httpAPI.Start(); err != nil {
|
||||
log.Error("failed to start API", err)
|
||||
}
|
||||
|
||||
if err := runUpgradesOnSchedule(c, filter, filterDesc, updateLock); err != nil {
|
||||
log.Error(err)
|
||||
var firstScan time.Time
|
||||
var scheduler *cron.Cron
|
||||
if enableScheduler {
|
||||
var err error
|
||||
scheduler, err = runUpgradesOnSchedule(up, &updateLock)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to start scheduler: %v", err)
|
||||
} else {
|
||||
firstScan = scheduler.Entries()[0].Schedule.Next(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
writeStartupMessage(c, firstScan, filterDesc)
|
||||
|
||||
// Graceful shut-down on SIGINT/SIGTERM
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
signal.Notify(interrupt, syscall.SIGTERM)
|
||||
|
||||
recievedSignal := <-interrupt
|
||||
localLog.WithField("signal", recievedSignal).Infof("Got shutdown signal. Gracefully shutting down...")
|
||||
if scheduler != nil {
|
||||
scheduler.Stop()
|
||||
}
|
||||
|
||||
updateLock.Lock()
|
||||
go func() {
|
||||
time.Sleep(time.Second * 3)
|
||||
updateLock.Unlock()
|
||||
}()
|
||||
|
||||
waitFor(httpAPI.Stop(), "Waiting for HTTP API requests to complete...")
|
||||
waitFor(&updateLock, "Waiting for running updates to be finished...")
|
||||
|
||||
localLog.Info("Shutdown completed")
|
||||
}
|
||||
|
||||
func waitFor(waitLock *sync.Mutex, delayMessage string) {
|
||||
if !waitLock.TryLock() {
|
||||
log.Info(delayMessage)
|
||||
waitLock.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func logNotifyExit(err error) {
|
||||
|
|
@ -221,48 +250,9 @@ func awaitDockerClient() {
|
|||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
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) {
|
||||
noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message")
|
||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-updates")
|
||||
|
||||
var startupLog *log.Entry
|
||||
if noStartupMessage {
|
||||
|
|
@ -285,11 +275,11 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
|
|||
startupLog.Info(filtering)
|
||||
|
||||
if !sched.IsZero() {
|
||||
until := formatDuration(time.Until(sched))
|
||||
until := util.FormatDuration(time.Until(sched))
|
||||
startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST"))
|
||||
startupLog.Info("Note that the first check will be performed in " + until)
|
||||
} else if runOnce, _ := c.PersistentFlags().GetBool("run-once"); runOnce {
|
||||
startupLog.Info("Running a one time update.")
|
||||
startupLog.Info("Running a one time updates.")
|
||||
} else {
|
||||
startupLog.Info("Periodic runs are not enabled.")
|
||||
}
|
||||
|
|
@ -309,25 +299,19 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
|
|||
}
|
||||
}
|
||||
|
||||
func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, lock chan bool) error {
|
||||
if lock == nil {
|
||||
lock = make(chan bool, 1)
|
||||
lock <- true
|
||||
}
|
||||
|
||||
func runUpgradesOnSchedule(updateParams t.UpdateParams, updateLock *sync.Mutex) (*cron.Cron, error) {
|
||||
scheduler := cron.New()
|
||||
err := scheduler.AddFunc(
|
||||
scheduleSpec,
|
||||
func() {
|
||||
select {
|
||||
case v := <-lock:
|
||||
defer func() { lock <- v }()
|
||||
metric := runUpdatesWithNotifications(filter)
|
||||
metrics.RegisterScan(metric)
|
||||
default:
|
||||
if updateLock.TryLock() {
|
||||
defer updateLock.Unlock()
|
||||
result := runUpdatesWithNotifications(updateParams)
|
||||
metrics.RegisterScan(metrics.NewMetric(result))
|
||||
} else {
|
||||
// Update was skipped
|
||||
metrics.RegisterScan(nil)
|
||||
log.Debug("Skipped another update already running.")
|
||||
log.Debug("Skipped another updates already running.")
|
||||
}
|
||||
|
||||
nextRuns := scheduler.Entries()
|
||||
|
|
@ -337,47 +321,28 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string,
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writeStartupMessage(c, scheduler.Entries()[0].Schedule.Next(time.Now()), filtering)
|
||||
|
||||
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
|
||||
scheduler.Stop()
|
||||
log.Info("Waiting for running update to be finished...")
|
||||
<-lock
|
||||
return nil
|
||||
return scheduler, nil
|
||||
}
|
||||
|
||||
func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
||||
func runUpdatesWithNotifications(updateParams t.UpdateParams) t.Report {
|
||||
notifier.StartNotification()
|
||||
updateParams := t.UpdateParams{
|
||||
Filter: filter,
|
||||
Cleanup: cleanup,
|
||||
NoRestart: noRestart,
|
||||
Timeout: timeout,
|
||||
MonitorOnly: monitorOnly,
|
||||
LifecycleHooks: lifecycleHooks,
|
||||
RollingRestart: rollingRestart,
|
||||
LabelPrecedence: labelPrecedence,
|
||||
}
|
||||
|
||||
result, err := actions.Update(client, updateParams)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
notifier.SendNotification(result)
|
||||
metricResults := metrics.NewMetric(result)
|
||||
notifications.LocalLog.WithFields(log.Fields{
|
||||
"Scanned": metricResults.Scanned,
|
||||
"Updated": metricResults.Updated,
|
||||
"Failed": metricResults.Failed,
|
||||
|
||||
localLog.WithFields(log.Fields{
|
||||
"Scanned": len(result.Scanned()),
|
||||
"Updated": len(result.Updated()),
|
||||
"Failed": len(result.Failed()),
|
||||
}).Info("Session done")
|
||||
return metricResults
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue