2019-06-22 22:04:36 +02:00
package cmd
import (
2019-07-21 18:00:56 +02:00
"os"
"os/signal"
2021-03-28 21:04:11 +02:00
"strings"
2023-10-21 19:35:41 +02:00
"sync"
2019-07-21 18:00:56 +02:00
"syscall"
"time"
2019-07-21 22:22:30 +02:00
"github.com/containrrr/watchtower/internal/actions"
2019-06-22 22:04:36 +02:00
"github.com/containrrr/watchtower/internal/flags"
2022-05-27 12:16:18 +02:00
"github.com/containrrr/watchtower/internal/meta"
2023-10-21 19:35:41 +02:00
"github.com/containrrr/watchtower/internal/util"
2020-04-20 11:17:14 -03:00
"github.com/containrrr/watchtower/pkg/api"
2023-10-21 19:35:41 +02:00
"github.com/containrrr/watchtower/pkg/api/updates"
2019-07-21 20:15:04 +02:00
"github.com/containrrr/watchtower/pkg/container"
2020-03-13 10:38:33 +01:00
"github.com/containrrr/watchtower/pkg/filters"
2021-03-28 21:04:11 +02:00
"github.com/containrrr/watchtower/pkg/metrics"
2019-07-21 22:22:30 +02:00
"github.com/containrrr/watchtower/pkg/notifications"
2019-07-21 19:58:19 +02:00
t "github.com/containrrr/watchtower/pkg/types"
2019-06-22 22:04:36 +02:00
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
2023-10-04 05:44:52 -03:00
client container . Client
scheduleSpec string
enableLabel bool
disableContainers [ ] string
notifier t . Notifier
scope string
2023-10-21 19:35:41 +02:00
up = t . UpdateParams { }
2019-06-22 22:04:36 +02:00
)
2021-01-06 20:06:56 +01:00
var rootCmd = NewRootCommand ( )
2023-10-21 19:35:41 +02:00
var localLog = notifications . LocalLog
2021-01-06 20:06:56 +01:00
// NewRootCommand creates the root command for watchtower
func NewRootCommand ( ) * cobra . Command {
return & cobra . Command {
Use : "watchtower" ,
Short : "Automatically updates running Docker containers" ,
Long : `
Watchtower automatically updates running Docker containers whenever a new image is released .
More information available at https : //github.com/containrrr/watchtower/.
` ,
Run : Run ,
PreRun : PreRun ,
2022-11-05 19:06:52 +01:00
Args : cobra . ArbitraryArgs ,
2021-01-06 20:06:56 +01:00
}
2019-06-22 22:04:36 +02:00
}
func init ( ) {
2020-12-21 23:08:23 +01:00
flags . SetDefaults ( )
2019-06-22 22:04:36 +02:00
flags . RegisterDockerFlags ( rootCmd )
flags . RegisterSystemFlags ( rootCmd )
flags . RegisterNotificationFlags ( rootCmd )
}
2019-06-23 00:32:50 +02:00
// Execute the root func and exit in case of errors
2019-06-22 22:04:36 +02:00
func Execute ( ) {
2022-11-01 00:00:00 +01:00
rootCmd . AddCommand ( notifyUpgradeCommand )
2019-06-22 22:04:36 +02:00
if err := rootCmd . Execute ( ) ; err != nil {
2019-06-23 00:32:50 +02:00
log . Fatal ( err )
2019-06-22 22:04:36 +02:00
}
}
2019-06-23 00:32:50 +02:00
// PreRun is a lifecycle hook that runs before the command is executed.
2021-03-28 21:04:11 +02:00
func PreRun ( cmd * cobra . Command , _ [ ] string ) {
2020-12-21 23:08:23 +01:00
f := cmd . PersistentFlags ( )
2022-08-14 10:11:31 +02:00
flags . ProcessFlagAliases ( f )
2023-09-16 18:23:26 +03:00
if err := flags . SetupLogging ( f ) ; err != nil {
log . Fatalf ( "Failed to initialize logging: %s" , err . Error ( ) )
2020-05-11 06:09:52 +02:00
}
2019-06-22 22:04:36 +02:00
2022-08-14 10:11:31 +02:00
scheduleSpec , _ = f . GetString ( "schedule" )
2019-06-22 22:04:36 +02:00
2020-12-21 23:08:23 +01:00
flags . GetSecretsFromFiles ( cmd )
2023-10-21 19:35:41 +02:00
up . Cleanup , up . NoRestart , up . MonitorOnly , up . Timeout = flags . ReadFlags ( cmd )
2020-12-21 15:17:45 +01:00
2023-10-21 19:35:41 +02:00
if up . Timeout < 0 {
2019-06-22 22:04:36 +02:00
log . Fatal ( "Please specify a positive value for timeout value." )
}
2019-07-27 01:37:16 +02:00
2020-12-21 23:08:23 +01:00
enableLabel , _ = f . GetBool ( "label-enable" )
2023-10-04 05:44:52 -03:00
disableContainers , _ = f . GetStringSlice ( "disable-containers" )
2023-10-21 19:35:41 +02:00
up . LifecycleHooks , _ = f . GetBool ( "enable-lifecycle-hooks" )
up . RollingRestart , _ = f . GetBool ( "rolling-restart" )
2020-12-21 23:08:23 +01:00
scope , _ = f . GetString ( "scope" )
2023-10-21 19:35:41 +02:00
up . LabelPrecedence , _ = f . GetBool ( "label-take-precedence" )
2020-12-21 23:08:23 +01:00
2022-08-14 10:11:31 +02:00
if scope != "" {
log . Debugf ( ` Using scope %q ` , scope )
}
2019-06-22 22:04:36 +02:00
2020-12-21 23:08:23 +01:00
// configure environment vars for client
err := flags . EnvConfig ( cmd )
if err != nil {
log . Fatal ( err )
2019-06-22 22:04:36 +02:00
}
2023-10-21 19:35:41 +02:00
var clientOpts = container . ClientOptions { }
2020-12-21 23:08:23 +01:00
noPull , _ := f . GetBool ( "no-pull" )
2023-10-21 19:35:41 +02:00
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" )
2021-04-23 16:34:21 +02:00
warnOnHeadPullFailed , _ := f . GetString ( "warn-on-head-failure" )
2023-10-21 19:35:41 +02:00
clientOpts . WarnOnHeadFailed = container . WarningStrategy ( warnOnHeadPullFailed )
2020-12-21 23:08:23 +01:00
2023-10-21 19:35:41 +02:00
if up . MonitorOnly && noPull {
2020-08-08 20:43:01 +04:00
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." )
}
2023-10-21 19:35:41 +02:00
client = container . NewClient ( clientOpts )
2019-06-22 22:04:36 +02:00
notifier = notifications . NewNotifier ( cmd )
2022-11-01 00:00:00 +01:00
notifier . AddLogHook ( )
2019-06-22 22:04:36 +02:00
}
2019-06-23 00:32:50 +02:00
// Run is the main execution flow of the command
2020-12-21 23:08:23 +01:00
func Run ( c * cobra . Command , names [ ] string ) {
2023-10-04 05:44:52 -03:00
filter , filterDesc := filters . BuildFilter ( names , disableContainers , enableLabel , scope )
2023-10-21 19:35:41 +02:00
up . Filter = filter
2020-12-21 23:08:23 +01:00
runOnce , _ := c . PersistentFlags ( ) . GetBool ( "run-once" )
2023-10-21 19:35:41 +02:00
enableUpdateAPI , _ := c . PersistentFlags ( ) . GetBool ( "http-api-updates" )
2021-01-06 22:28:32 +01:00
enableMetricsAPI , _ := c . PersistentFlags ( ) . GetBool ( "http-api-metrics" )
2021-04-27 22:18:45 +02:00
unblockHTTPAPI , _ := c . PersistentFlags ( ) . GetBool ( "http-api-periodic-polls" )
2021-01-06 22:28:32 +01:00
apiToken , _ := c . PersistentFlags ( ) . GetString ( "http-api-token" )
2023-09-16 21:10:00 +02:00
healthCheck , _ := c . PersistentFlags ( ) . GetBool ( "health-check" )
2023-10-21 19:35:41 +02:00
enableScheduler := ! enableUpdateAPI || unblockHTTPAPI
2023-09-16 21:10:00 +02:00
if healthCheck {
// health check should not have pid 1
if os . Getpid ( ) == 1 {
time . Sleep ( 1 * time . Second )
log . Fatal ( "The health check flag should never be passed to the main watchtower container process" )
}
os . Exit ( 0 )
}
2020-04-20 11:17:14 -03:00
2023-10-21 19:35:41 +02:00
if up . RollingRestart && up . MonitorOnly {
2021-04-18 18:37:35 +02:00
log . Fatal ( "Rolling restarts is not compatible with the global monitor only flag" )
}
awaitDockerClient ( )
2023-10-21 19:35:41 +02:00
if err := actions . CheckForSanity ( client , up . Filter , up . RollingRestart ) ; err != nil {
2021-04-18 18:37:35 +02:00
logNotifyExit ( err )
}
2020-12-21 23:08:23 +01:00
if runOnce {
2021-03-28 21:04:11 +02:00
writeStartupMessage ( c , time . Time { } , filterDesc )
2023-10-21 19:35:41 +02:00
runUpdatesWithNotifications ( up )
2020-08-08 22:55:51 +02:00
notifier . Close ( )
2019-08-25 13:02:08 +02:00
os . Exit ( 0 )
2019-06-22 22:04:36 +02:00
return
}
2023-10-21 19:35:41 +02:00
if err := actions . CheckForMultipleWatchtowerInstances ( client , up . Cleanup , scope ) ; err != nil {
2021-04-18 18:37:35 +02:00
logNotifyExit ( err )
2019-06-22 22:04:36 +02:00
}
2023-10-21 19:35:41 +02:00
// The lock is shared between the scheduler and the HTTP API. It only allows one updates to run at a time.
updateLock := sync . Mutex { }
2021-04-27 22:18:45 +02:00
2021-01-06 22:28:32 +01:00
httpAPI := api . New ( apiToken )
2020-12-21 23:08:23 +01:00
2021-01-06 22:28:32 +01:00
if enableUpdateAPI {
2023-10-21 19:35:41 +02:00
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 )
2021-01-06 22:28:32 +01:00
}
2020-08-21 15:13:47 -03:00
2021-01-06 22:28:32 +01:00
if enableMetricsAPI {
2023-10-21 19:35:41 +02:00
httpAPI . EnableMetrics ( )
2020-08-21 15:13:47 -03:00
}
2023-10-21 19:35:41 +02:00
if err := httpAPI . Start ( ) ; err != nil {
2021-03-28 21:04:11 +02:00
log . Error ( "failed to start API" , err )
}
2021-01-06 22:28:32 +01:00
2023-10-21 19:35:41 +02:00
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 ( ) )
}
2019-07-22 12:10:57 +02:00
}
2023-10-21 19:35:41 +02:00
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 ( )
}
2019-06-22 22:04:36 +02:00
}
2021-04-18 18:37:35 +02:00
func logNotifyExit ( err error ) {
log . Error ( err )
notifier . Close ( )
os . Exit ( 1 )
}
func awaitDockerClient ( ) {
log . Debug ( "Sleeping for a second to ensure the docker api client has been properly initialized." )
time . Sleep ( 1 * time . Second )
}
2021-03-28 21:04:11 +02:00
func writeStartupMessage ( c * cobra . Command , sched time . Time , filtering string ) {
2021-09-19 18:06:14 +02:00
noStartupMessage , _ := c . PersistentFlags ( ) . GetBool ( "no-startup-message" )
2023-10-21 19:35:41 +02:00
enableUpdateAPI , _ := c . PersistentFlags ( ) . GetBool ( "http-api-updates" )
2021-09-19 18:06:14 +02:00
var startupLog * log . Entry
if noStartupMessage {
startupLog = notifications . LocalLog
} else {
startupLog = log . NewEntry ( log . StandardLogger ( ) )
// Batch up startup messages to send them as a single notification
notifier . StartNotification ( )
}
startupLog . Info ( "Watchtower " , meta . Version )
notifierNames := notifier . GetNames ( )
if len ( notifierNames ) > 0 {
startupLog . Info ( "Using notifications: " + strings . Join ( notifierNames , ", " ) )
} else {
startupLog . Info ( "Using no notifications" )
}
startupLog . Info ( filtering )
if ! sched . IsZero ( ) {
2023-10-21 19:35:41 +02:00
until := util . FormatDuration ( time . Until ( sched ) )
2021-09-19 18:06:14 +02:00
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 )
2021-11-29 16:07:26 +02:00
} else if runOnce , _ := c . PersistentFlags ( ) . GetBool ( "run-once" ) ; runOnce {
2023-10-21 19:35:41 +02:00
startupLog . Info ( "Running a one time updates." )
2021-09-19 18:06:14 +02:00
} else {
2021-11-29 16:07:26 +02:00
startupLog . Info ( "Periodic runs are not enabled." )
}
if enableUpdateAPI {
// TODO: make listen port configurable
startupLog . Info ( "The HTTP API is enabled at :8080." )
2021-09-19 18:06:14 +02:00
}
if ! noStartupMessage {
// Send the queued up startup messages, not including the trace warning below (to make sure it's noticed)
notifier . SendNotification ( nil )
}
if log . IsLevelEnabled ( log . TraceLevel ) {
startupLog . Warn ( "Trace level enabled: log will include sensitive information as credentials and tokens" )
2021-03-28 21:04:11 +02:00
}
}
2023-10-21 19:35:41 +02:00
func runUpgradesOnSchedule ( updateParams t . UpdateParams , updateLock * sync . Mutex ) ( * cron . Cron , error ) {
2021-03-28 21:04:11 +02:00
scheduler := cron . New ( )
err := scheduler . AddFunc (
2020-12-21 23:08:23 +01:00
scheduleSpec ,
2019-06-22 22:04:36 +02:00
func ( ) {
2023-10-21 19:35:41 +02:00
if updateLock . TryLock ( ) {
defer updateLock . Unlock ( )
result := runUpdatesWithNotifications ( updateParams )
metrics . RegisterScan ( metrics . NewMetric ( result ) )
} else {
2021-01-06 22:28:32 +01:00
// Update was skipped
2021-03-28 21:04:11 +02:00
metrics . RegisterScan ( nil )
2023-10-21 19:35:41 +02:00
log . Debug ( "Skipped another updates already running." )
2019-06-22 22:04:36 +02:00
}
2021-03-28 21:04:11 +02:00
nextRuns := scheduler . Entries ( )
2019-06-22 22:04:36 +02:00
if len ( nextRuns ) > 0 {
log . Debug ( "Scheduled next run: " + nextRuns [ 0 ] . Next . String ( ) )
}
} )
if err != nil {
2023-10-21 19:35:41 +02:00
return nil , err
2019-06-22 22:04:36 +02:00
}
2021-03-28 21:04:11 +02:00
scheduler . Start ( )
2019-06-22 22:04:36 +02:00
2023-10-21 19:35:41 +02:00
return scheduler , nil
2019-06-22 22:04:36 +02:00
}
2023-10-21 19:35:41 +02:00
func runUpdatesWithNotifications ( updateParams t . UpdateParams ) t . Report {
2019-06-22 22:04:36 +02:00
notifier . StartNotification ( )
2023-10-21 19:35:41 +02:00
2021-06-27 09:05:01 +02:00
result , err := actions . Update ( client , updateParams )
2019-06-22 22:04:36 +02:00
if err != nil {
2021-04-24 18:29:05 +02:00
log . Error ( err )
2019-06-22 22:04:36 +02:00
}
2021-06-27 09:05:01 +02:00
notifier . SendNotification ( result )
2023-10-21 19:35:41 +02:00
localLog . WithFields ( log . Fields {
"Scanned" : len ( result . Scanned ( ) ) ,
"Updated" : len ( result . Updated ( ) ) ,
"Failed" : len ( result . Failed ( ) ) ,
2021-11-18 14:08:38 +01:00
} ) . Info ( "Session done" )
2023-10-21 19:35:41 +02:00
return result
2019-06-22 22:04:36 +02:00
}