package cmd
import (
"fmt"
"github.com/spf13/viper"
"os"
"os/signal"
"syscall"
"time"
"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/notifications"
t "github.com/containrrr/watchtower/pkg/types"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
client container . Client
notifier * notifications . Notifier
c flags . WatchConfig
)
var rootCmd = & 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 ,
}
func init ( ) {
flags . RegisterDockerFlags ( rootCmd )
flags . RegisterSystemFlags ( rootCmd )
flags . RegisterNotificationFlags ( rootCmd )
flags . SetEnvBindings ( )
flags . BindViperFlags ( rootCmd )
}
// Execute the root func and exit in case of errors
func Execute ( ) {
if err := rootCmd . Execute ( ) ; err != nil {
log . Fatal ( err )
}
}
// PreRun is a lifecycle hook that runs before the command is executed.
func PreRun ( cmd * cobra . Command , _ [ ] string ) {
// First apply all the settings that affect the output
if viper . GetBool ( "no-color" ) {
log . SetFormatter ( & log . TextFormatter {
DisableColors : true ,
} )
} else {
// enable logrus built-in support for https://bixense.com/clicolors/
log . SetFormatter ( & log . TextFormatter {
EnvironmentOverrideColors : true ,
} )
}
if viper . GetBool ( "debug" ) {
log . SetLevel ( log . DebugLevel )
}
if viper . GetBool ( "trace" ) {
log . SetLevel ( log . TraceLevel )
}
interval := viper . GetInt ( "interval" )
// If empty, set schedule using interval helper value
if viper . GetString ( "schedule" ) == "" {
viper . Set ( "schedule" , fmt . Sprintf ( "@every %ds" , interval ) )
} else if interval != flags . DefaultInterval {
log . Fatal ( "only schedule or interval can be defined, not both" )
}
// Then load the rest of the settings
err := viper . Unmarshal ( & c )
if err != nil {
log . Fatalf ( "unable to decode into struct, %v" , err )
}
flags . GetSecretsFromFiles ( )
if c . Timeout <= 0 {
log . Fatal ( "Please specify a positive value for timeout value." )
}
log . Debugf ( "Using scope %v" , c . Scope )
if err = flags . EnvConfig ( ) ; err != nil {
log . Fatalf ( "failed to setup environment variables: %v" , err )
}
if c . MonitorOnly && c . 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 ( & c )
notifier = notifications . NewNotifier ( cmd )
}
// Run is the main execution flow of the command
func Run ( _ * cobra . Command , names [ ] string ) {
filter := filters . BuildFilter ( names , c . EnableLabel , c . Scope )
if c . RunOnce {
if ! c . NoStartupMessage {
log . Info ( "Running a one time update." )
}
runUpdatesWithNotifications ( filter )
notifier . Close ( )
os . Exit ( 0 )
return
}
if err := actions . CheckForMultipleWatchtowerInstances ( client , c . Cleanup , c . Scope ) ; err != nil {
log . Fatal ( err )
}
if c . HTTPAPI {
if err := api . SetupHTTPUpdates ( c . HTTPAPIToken , func ( ) { runUpdatesWithNotifications ( filter ) } ) ; err != nil {
log . Fatal ( err )
os . Exit ( 1 )
}
api . WaitForHTTPUpdates ( )
}
if err := runUpgradesOnSchedule ( filter ) ; err != nil {
log . Error ( err )
}
os . Exit ( 1 )
}
func runUpgradesOnSchedule ( filter t . Filter ) error {
tryLockSem := make ( chan bool , 1 )
tryLockSem <- true
runner := cron . New ( )
err := runner . AddFunc (
viper . GetString ( "schedule" ) ,
func ( ) {
select {
case v := <- tryLockSem :
defer func ( ) { tryLockSem <- v } ( )
runUpdatesWithNotifications ( filter )
default :
log . Debug ( "Skipped another update already running." )
}
nextRuns := runner . Entries ( )
if len ( nextRuns ) > 0 {
log . Debug ( "Scheduled next run: " + nextRuns [ 0 ] . Next . String ( ) )
}
} )
if err != nil {
return err
}
if ! viper . GetBool ( "no-startup-message" ) {
log . Info ( "Starting Watchtower and scheduling first run: " + runner . Entries ( ) [ 0 ] . Schedule . Next ( time . Now ( ) ) . String ( ) )
}
runner . Start ( )
// Graceful shut-down on SIGINT/SIGTERM
interrupt := make ( chan os . Signal , 1 )
signal . Notify ( interrupt , os . Interrupt )
signal . Notify ( interrupt , syscall . SIGTERM )
<- interrupt
runner . Stop ( )
log . Info ( "Waiting for running update to be finished..." )
<- tryLockSem
return nil
}
func runUpdatesWithNotifications ( filter t . Filter ) {
notifier . StartNotification ( )
updateParams := t . UpdateParams {
Filter : filter ,
Cleanup : c . Cleanup ,
NoRestart : c . NoRestart ,
Timeout : c . Timeout ,
MonitorOnly : c . MonitorOnly ,
LifecycleHooks : c . LifecycleHooks ,
RollingRestart : c . RollingRestart ,
}
err := actions . Update ( client , updateParams )
if err != nil {
log . Println ( err )
}
notifier . SendNotification ( )
}