diff --git a/cmd/root.go b/cmd/root.go index 1be52a8..1e8896c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/containrrr/watchtower/internal/meta" "math" "net/http" "os" @@ -11,36 +10,28 @@ import ( "syscall" "time" - 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/internal/meta" "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/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" log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" + "github.com/spf13/viper" ) var ( - client container.Client - scheduleSpec string - cleanup bool - noRestart bool - monitorOnly bool - enableLabel bool - notifier t.Notifier - timeout time.Duration - lifecycleHooks bool - rollingRestart bool - scope string - // Set on build using ldflags + client container.Client + notifier *notifications.Notifier + c flags.WatchConfig ) var rootCmd = NewRootCommand() @@ -60,10 +51,11 @@ func NewRootCommand() *cobra.Command { } func init() { - flags.SetDefaults() flags.RegisterDockerFlags(rootCmd) flags.RegisterSystemFlags(rootCmd) flags.RegisterNotificationFlags(rootCmd) + flags.SetEnvBindings() + flags.BindViperFlags(rootCmd) } // Execute the root func and exit in case of errors @@ -75,9 +67,9 @@ func Execute() { // PreRun is a lifecycle hook that runs before the command is executed. func PreRun(cmd *cobra.Command, _ []string) { - f := cmd.PersistentFlags() - if enabled, _ := f.GetBool("no-color"); enabled { + // First apply all the settings that affect the output + if viper.GetBool("no-color") { log.SetFormatter(&log.TextFormatter{ DisableColors: true, }) @@ -88,97 +80,72 @@ func PreRun(cmd *cobra.Command, _ []string) { }) } - if enabled, _ := f.GetBool("debug"); enabled { + if viper.GetBool("debug") { log.SetLevel(log.DebugLevel) } - if enabled, _ := f.GetBool("trace"); enabled { + if viper.GetBool("trace") { log.SetLevel(log.TraceLevel) } - pollingSet := f.Changed("interval") - schedule, _ := f.GetString("schedule") - cronLen := len(schedule) + interval := viper.GetInt("interval") - 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" + // 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") } - flags.GetSecretsFromFiles(cmd) - cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd) + // 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 timeout < 0 { + if c.Timeout <= 0 { log.Fatal("Please specify a positive value for timeout value.") } - enableLabel, _ = f.GetBool("label-enable") - lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") - rollingRestart, _ = f.GetBool("rolling-restart") - scope, _ = f.GetString("scope") + log.Debugf("Using scope %v", c.Scope) - log.Debug(scope) - - // configure environment vars for client - err := flags.EnvConfig(cmd) - if err != nil { - log.Fatal(err) + if err = flags.EnvConfig(); err != nil { + log.Fatalf("failed to setup environment variables: %v", 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") - - if monitorOnly && noPull { + 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( - !noPull, - includeStopped, - reviveStopped, - removeVolumes, - includeRestarting, - warnOnHeadPullFailed, - ) + client = container.NewClient(&c) - notifier = notifications.NewNotifier(cmd) + notifier = notifications.NewNotifier() } // Run is the main execution flow of the command -func Run(c *cobra.Command, names []string) { - 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") - unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls") - apiToken, _ := c.PersistentFlags().GetString("http-api-token") - - if rollingRestart && monitorOnly { +func Run(_ *cobra.Command, names []string) { + filter, filterDesc := filters.BuildFilter(names, c.EnableLabel, c.Scope) + + if c.RollingRestart && c.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, filter, c.RollingRestart); err != nil { logNotifyExit(err) } - if runOnce { - writeStartupMessage(c, time.Time{}, filterDesc) + if c.RunOnce { + writeStartupMessage(time.Time{}, filterDesc) runUpdatesWithNotifications(filter) notifier.Close() os.Exit(0) return } - if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil { + if err := actions.CheckForMultipleWatchtowerInstances(client, c.Cleanup, c.Scope); err != nil { logNotifyExit(err) } @@ -186,28 +153,28 @@ func Run(c *cobra.Command, names []string) { updateLock := make(chan bool, 1) updateLock <- true - httpAPI := api.New(apiToken) + httpAPI := api.New(c.HTTPAPIToken) - if enableUpdateAPI { + if c.EnableUpdateAPI { updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, 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 { + if !c.UpdateAPIWithScheduler { writeStartupMessage(c, time.Time{}, filterDesc) } } - if enableMetricsAPI { + if c.EnableMetricsAPI { metricsHandler := apiMetrics.New() httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) } - if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed { + if err := httpAPI.Start(c.EnableUpdateAPI && !c.UpdateAPIWithScheduler); err != nil { log.Error("failed to start API", err) } - if err := runUpgradesOnSchedule(c, filter, filterDesc, updateLock); err != nil { + if err := runUpgradesOnSchedule(filter, filterDesc, updateLock); err != nil { log.Error(err) } @@ -265,11 +232,8 @@ func formatDuration(d time.Duration) 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") - var startupLog *log.Entry - if noStartupMessage { + if c.NoStartupMessage { startupLog = notifications.LocalLog } else { startupLog = log.NewEntry(log.StandardLogger()) @@ -298,12 +262,12 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) { startupLog.Info("Periodic runs are not enabled.") } - if enableUpdateAPI { + if c.EnableUpdateAPI { // TODO: make listen port configurable startupLog.Info("The HTTP API is enabled at :8080.") } - if !noStartupMessage { + if !c.NoStartupMessage { // Send the queued up startup messages, not including the trace warning below (to make sure it's noticed) notifier.SendNotification(nil) } @@ -313,7 +277,7 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) { } } -func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, lock chan bool) error { +func runUpgradesOnSchedule(filter t.Filter, filtering string, lock chan bool) error { if lock == nil { lock = make(chan bool, 1) lock <- true @@ -321,7 +285,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, scheduler := cron.New() err := scheduler.AddFunc( - scheduleSpec, + c.Schedule, func() { select { case v := <-lock: @@ -344,7 +308,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, return err } - writeStartupMessage(c, scheduler.Entries()[0].Schedule.Next(time.Now()), filtering) + writeStartupMessage(scheduler.Entries()[0].Schedule.Next(time.Now()), filtering) scheduler.Start() @@ -364,12 +328,12 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { notifier.StartNotification() updateParams := t.UpdateParams{ Filter: filter, - Cleanup: cleanup, - NoRestart: noRestart, - Timeout: timeout, - MonitorOnly: monitorOnly, - LifecycleHooks: lifecycleHooks, - RollingRestart: rollingRestart, + Cleanup: c.Cleanup, + NoRestart: c.NoRestart, + Timeout: c.Timeout, + MonitorOnly: c.MonitorOnly, + LifecycleHooks: c.LifecycleHooks, + RollingRestart: c.RollingRestart, } result, err := actions.Update(client, updateParams) if err != nil { diff --git a/internal/flags/config.go b/internal/flags/config.go new file mode 100644 index 0000000..05f7d01 --- /dev/null +++ b/internal/flags/config.go @@ -0,0 +1,34 @@ +package flags + +import ( + . "time" +) + +// WatchConfig is the global watchtower configuration created from flags and environment variables +type WatchConfig struct { + Interval int + Schedule string + NoPull bool `mapstructure:"no-pull"` + NoRestart bool `mapstructure:"no-restart"` + NoStartupMessage bool `mapstructure:"no-startup-message"` + Cleanup bool + RemoveVolumes bool `mapstructure:"remove-volumes"` + EnableLabel bool `mapstructure:"label-enable"` + Debug bool + Trace bool + MonitorOnly bool `mapstructure:"monitor-only"` + RunOnce bool `mapstructure:"run-once"` + IncludeStopped bool `mapstructure:"include-stopped"` + IncludeRestarting bool `mapstructure:"include-restarting"` + ReviveStopped bool `mapstructure:"revive-stopped"` + LifecycleHooks bool `mapstructure:"enable-lifecycle-hooks"` + RollingRestart bool `mapstructure:"rolling-restart"` + HTTPAPIToken string `mapstructure:"http-api-token"` + Timeout Duration `mapstructure:"stop-timeout"` + Scope string + EnableUpdateAPI bool `mapstructure:"http-api-update"` + EnableMetricsAPI bool `mapstructure:"http-api-metrics"` + UpdateAPIWithScheduler bool `mapstructure:"http-api-periodic-polls"` + WarnOnHeadFailed string `mapstructure:"warn-on-head-failure"` + NoColor bool `mapstructure:"no-color"` +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 003ec30..0cd9812 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -15,13 +15,14 @@ import ( // DockerAPIMinVersion is the minimum version of the docker api required to // use watchtower const DockerAPIMinVersion string = "1.25" +const DefaultInterval = int(time.Hour * 24 / time.Second) // RegisterDockerFlags that are used directly by the docker api client func RegisterDockerFlags(rootCmd *cobra.Command) { flags := rootCmd.PersistentFlags() - flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to") - flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote") - flags.StringP("api-version", "a", viper.GetString("DOCKER_API_VERSION"), "api version to use by docker client") + flags.StringP("host", "H", "unix:///var/run/docker.sock", "daemon socket to connect to") + flags.BoolP("tlsverify", "v", false, "use TLS and verify the remote") + flags.StringP("api-version", "a", DockerAPIMinVersion, "api version to use by docker client") } // RegisterSystemFlags that are used by watchtower to modify the program flow @@ -30,126 +31,118 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.IntP( "interval", "i", - viper.GetInt("WATCHTOWER_POLL_INTERVAL"), - "Poll interval (in seconds)") + DefaultInterval, // viper.GetInt("WATCHTOWER_POLL_INTERVAL"), + "poll interval (in seconds)") - flags.StringP( + flags.String( "schedule", "s", - viper.GetString("WATCHTOWER_SCHEDULE"), + "", "The cron expression which defines when to update") + //viper.GetString("WATCHTOWER_SCHEDULE"), flags.DurationP( "stop-timeout", "t", - viper.GetDuration("WATCHTOWER_TIMEOUT"), + time.Second*10, //viper.GetDuration("WATCHTOWER_TIMEOUT"), "Timeout before a container is forcefully stopped") flags.BoolP( "no-pull", "", - viper.GetBool("WATCHTOWER_NO_PULL"), + false, // viper.GetBool("WATCHTOWER_NO_PULL"), "Do not pull any new images") - flags.BoolP( + flags.Bool( "no-restart", - "", - viper.GetBool("WATCHTOWER_NO_RESTART"), + false, // viper.GetBool("WATCHTOWER_NO_RESTART"), "Do not restart any containers") - flags.BoolP( + flags.Bool( "no-startup-message", - "", - viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), + false, // viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), "Prevents watchtower from sending a startup message") flags.BoolP( "cleanup", "c", - viper.GetBool("WATCHTOWER_CLEANUP"), + false, // viper.GetBool("WATCHTOWER_CLEANUP"), "Remove previously used images after updating") flags.BoolP( "remove-volumes", "", - viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), + false, // viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), "Remove attached volumes before updating") flags.BoolP( "label-enable", "e", - viper.GetBool("WATCHTOWER_LABEL_ENABLE"), + false, // viper.GetBool("WATCHTOWER_LABEL_ENABLE"), "Watch containers where the com.centurylinklabs.watchtower.enable label is true") flags.BoolP( "debug", "d", - viper.GetBool("WATCHTOWER_DEBUG"), + false, // viper.GetBool("WATCHTOWER_DEBUG"), "Enable debug mode with verbose logging") - flags.BoolP( + flags.Bool( "trace", - "", - viper.GetBool("WATCHTOWER_TRACE"), + false, // viper.GetBool("WATCHTOWER_TRACE"), "Enable trace mode with very verbose logging - caution, exposes credentials") flags.BoolP( "monitor-only", "m", - viper.GetBool("WATCHTOWER_MONITOR_ONLY"), + false, // viper.GetBool("WATCHTOWER_MONITOR_ONLY"), "Will only monitor for new images, not update the containers") flags.BoolP( "run-once", "R", - viper.GetBool("WATCHTOWER_RUN_ONCE"), + false, // viper.GetBool("WATCHTOWER_RUN_ONCE"), "Run once now and exit") flags.BoolP( "include-restarting", "", - viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"), + false, // viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"), "Will also include restarting containers") flags.BoolP( "include-stopped", "S", - viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), + false, // viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), "Will also include created and exited containers") - flags.BoolP( + flags.Bool( "revive-stopped", - "", - viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), + false, // viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), "Will also start stopped containers that were updated, if include-stopped is active") - flags.BoolP( + flags.Bool( "enable-lifecycle-hooks", - "", - viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), + false, // viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") - flags.BoolP( + flags.Bool( "rolling-restart", - "", - viper.GetBool("WATCHTOWER_ROLLING_RESTART"), + false, // viper.GetBool("WATCHTOWER_ROLLING_RESTART"), "Restart containers one at a time") - flags.BoolP( + flags.Bool( "http-api-update", - "", - viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"), + false, // viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"), "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") - flags.BoolP( + flags.Bool( "http-api-metrics", - "", - viper.GetBool("WATCHTOWER_HTTP_API_METRICS"), + false, // viper.GetBool("WATCHTOWER_HTTP_API_METRICS"), "Runs Watchtower with the Prometheus metrics API enabled") - flags.StringP( + flags.String( "http-api-token", - "", - viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), + "", // viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), "Sets an authentication token to HTTP API requests.") flags.BoolP( "http-api-periodic-polls", @@ -160,12 +153,11 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.BoolP( "no-color", "", - viper.IsSet("NO_COLOR"), + false, // viper.IsSet("NO_COLOR"), "Disable ANSI color escape codes in log output") - flags.StringP( + flags.String( "scope", - "", - viper.GetString("WATCHTOWER_SCOPE"), + "", // viper.GetString("WATCHTOWER_SCOPE"), "Defines a monitoring scope for the Watchtower instance.") } @@ -176,159 +168,162 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { flags.StringSliceP( "notifications", "n", - viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), + []string{}, // viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), " Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.String( "notifications-level", - viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), + "info", // viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") - flags.IntP( + flags.Int( "notifications-delay", - "", - viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"), + 0, // viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"), "Delay before sending notifications, expressed in seconds") - flags.StringP( + flags.String( "notifications-hostname", "", - viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), + // viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), "Custom hostname for notification titles") - flags.StringP( + flags.String( "notification-email-from", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), + // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), "Address to send notification emails from") - flags.StringP( + flags.String( "notification-email-to", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), + // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), "Address to send notification emails to") - flags.IntP( + flags.Int( "notification-email-delay", - "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), + 0, + //viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), "Delay before sending notifications, expressed in seconds") - flags.StringP( + flags.String( "notification-email-server", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), + // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), "SMTP server to send notification emails through") - flags.IntP( + flags.Int( "notification-email-server-port", - "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), + 25, + // viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), "SMTP server port to send notification emails through") - flags.BoolP( + flags.Bool( "notification-email-server-tls-skip-verify", - "", - viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"), + false, + // viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"), `Controls whether watchtower verifies the SMTP server's certificate chain and host name. Should only be used for testing.`) - flags.StringP( + flags.String( "notification-email-server-user", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), + // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), "SMTP server user for sending notifications") - flags.StringP( + flags.String( "notification-email-server-password", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), + // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), "SMTP server password for sending notifications") - flags.StringP( + flags.String( "notification-email-subjecttag", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), + // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), "Subject prefix tag for notifications via mail") - flags.StringP( + flags.String( "notification-slack-hook-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), + // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), "The Slack Hook URL to send notifications to") - flags.StringP( + flags.String( "notification-slack-identifier", - "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), + "watchtower", + // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), "A string which will be used to identify the messages coming from this watchtower instance") - flags.StringP( + flags.String( "notification-slack-channel", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"), + // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"), "A string which overrides the webhook's default channel. Example: #my-custom-channel") - flags.StringP( + flags.String( "notification-slack-icon-emoji", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"), + // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"), "An emoji code string to use in place of the default icon") - flags.StringP( + flags.String( "notification-slack-icon-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"), + // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"), "An icon image URL string to use in place of the default icon") - flags.StringP( + flags.String( "notification-msteams-hook", "", - viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"), + // viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"), "The MSTeams WebHook URL to send notifications to") - flags.BoolP( + flags.Bool( "notification-msteams-data", - "", - viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), + false, + // viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), "The MSTeams notifier will try to extract log entry fields as MSTeams message facts") - flags.StringP( + flags.String( "notification-gotify-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), + // viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), "The Gotify URL to send notifications to") - flags.StringP( + flags.String( "notification-gotify-token", "", - viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), + // viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), "The Gotify Application required to query the Gotify API") - flags.BoolP( + flags.Bool( "notification-gotify-tls-skip-verify", - "", - viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), + false, + // viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), `Controls whether watchtower verifies the Gotify server's certificate chain and host name. Should only be used for testing.`) flags.String( "notification-template", - viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), + "", + // viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), "The shoutrrr text/template for the messages") flags.StringArray( "notification-url", - viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), + []string{}, + // viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), "The shoutrrr URL to send notifications to") flags.Bool("notification-report", - viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"), + false, + // viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"), "Use the session report as the notification template data") flags.String( "warn-on-head-failure", - viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), + "auto", + // viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), "When to warn about HEAD pull requests failing. Possible values: always, auto or never") } @@ -337,36 +332,23 @@ Should only be used for testing.`) func SetDefaults() { day := (time.Hour * 24).Seconds() viper.AutomaticEnv() - viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") - viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) - viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day) - viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10) - viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{}) - viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info") - viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25) - viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "") - viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower") +} + +// BindViperFlags binds the cmd PFlags to the viper configuration +func BindViperFlags(cmd *cobra.Command) { + if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { + log.Fatalf("failed to bind flags: %v", err) + } } // EnvConfig translates the command-line options into environment variables // that will initialize the api client -func EnvConfig(cmd *cobra.Command) error { +func EnvConfig() error { var err error - var host string - var tls bool - var version string - - flags := cmd.PersistentFlags() - if host, err = flags.GetString("host"); err != nil { - return err - } - if tls, err = flags.GetBool("tlsverify"); err != nil { - return err - } - if version, err = flags.GetString("api-version"); err != nil { - return err - } + host := viper.GetString("host") + tls = viper.GetBool("tlsverify") + version = viper.GetString("api-version") if err = setEnvOptStr("DOCKER_HOST", host); err != nil { return err } @@ -380,29 +362,14 @@ func EnvConfig(cmd *cobra.Command) error { } // ReadFlags reads common flags used in the main program flow of watchtower -func ReadFlags(cmd *cobra.Command) (bool, bool, bool, time.Duration) { - flags := cmd.PersistentFlags() - - var err error - var cleanup bool - var noRestart bool - var monitorOnly bool - var timeout time.Duration +func ReadFlags() (cleanup bool, noRestart bool, monitorOnly bool, timeout time.Duration) { - if cleanup, err = flags.GetBool("cleanup"); err != nil { - log.Fatal(err) - } - if noRestart, err = flags.GetBool("no-restart"); err != nil { - log.Fatal(err) - } - if monitorOnly, err = flags.GetBool("monitor-only"); err != nil { - log.Fatal(err) - } - if timeout, err = flags.GetDuration("stop-timeout"); err != nil { - log.Fatal(err) - } + cleanup = viper.GetBool("cleanup") + noRestart = viper.GetBool("no-restart") + monitorOnly = viper.GetBool("monitor-only") + timeout = viper.GetDuration("stop-timeout") - return cleanup, noRestart, monitorOnly, timeout + return } func setEnvOptStr(env string, opt string) error { @@ -425,9 +392,7 @@ func setEnvOptBool(env string, opt bool) error { // GetSecretsFromFiles checks if passwords/tokens/webhooks have been passed as a file instead of plaintext. // If so, the value of the flag will be replaced with the contents of the file. -func GetSecretsFromFiles(rootCmd *cobra.Command) { - flags := rootCmd.PersistentFlags() - +func GetSecretsFromFiles() { secrets := []string{ "notification-email-server-password", "notification-slack-hook-url", @@ -435,25 +400,19 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) { "notification-gotify-token", } for _, secret := range secrets { - getSecretFromFile(flags, secret) + getSecretFromFile(secret) } } // getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file. -func getSecretFromFile(flags *pflag.FlagSet, secret string) { - value, err := flags.GetString(secret) - if err != nil { - log.Error(err) - } +func getSecretFromFile(secret string) { + value := viper.GetString(secret) if value != "" && isFile(value) { file, err := ioutil.ReadFile(value) if err != nil { log.Fatal(err) } - err = flags.Set(secret, strings.TrimSpace(string(file))) - if err != nil { - log.Error(err) - } + viper.Set(secret, strings.TrimSpace(string(file))) } }