From ff8cb884a0852ce35a18f59802826f669c740236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Mon, 21 Dec 2020 15:17:45 +0100 Subject: [PATCH] feat(config): swap viper and cobra for config (#684) --- cmd/root.go | 136 ++++++++------------ internal/flags/config.go | 31 +++++ internal/flags/flags.go | 191 ++++++++++++----------------- internal/flags/flags_test.go | 19 +-- pkg/container/client.go | 13 +- pkg/notifications/email.go | 26 ++-- pkg/notifications/gotify.go | 11 +- pkg/notifications/msteams.go | 17 ++- pkg/notifications/notifier.go | 7 +- pkg/notifications/shoutrrr.go | 13 +- pkg/notifications/shoutrrr_test.go | 6 +- pkg/notifications/slack.go | 14 +-- 12 files changed, 229 insertions(+), 255 deletions(-) create mode 100644 internal/flags/config.go diff --git a/cmd/root.go b/cmd/root.go index 1e61308..d0fa413 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,9 +1,10 @@ package cmd import ( + "fmt" + "github.com/spf13/viper" "os" "os/signal" - "strconv" "syscall" "time" @@ -21,17 +22,9 @@ import ( ) var ( - client container.Client - scheduleSpec string - cleanup bool - noRestart bool - monitorOnly bool - enableLabel bool - notifier *notifications.Notifier - timeout time.Duration - lifecycleHooks bool - rollingRestart bool - scope string + client container.Client + notifier *notifications.Notifier + c flags.WatchConfig ) var rootCmd = &cobra.Command{ @@ -46,10 +39,11 @@ More information available at https://github.com/containrrr/watchtower/. } 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 @@ -60,10 +54,10 @@ func Execute() { } // PreRun is a lifecycle hook that runs before the command is executed. -func PreRun(cmd *cobra.Command, args []string) { - f := cmd.PersistentFlags() +func PreRun(cmd *cobra.Command, _ []string) { - 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, }) @@ -74,75 +68,55 @@ func PreRun(cmd *cobra.Command, args []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) + } - if timeout < 0 { + flags.GetSecretsFromFiles() + + 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.Debug(scope) + log.Debugf("Using scope %v", c.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") - - 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, - ) + client = container.NewClient(&c) notifier = notifications.NewNotifier(cmd) } // Run is the main execution flow of the command -func Run(c *cobra.Command, names []string) { - filter := filters.BuildFilter(names, enableLabel, scope) - runOnce, _ := c.PersistentFlags().GetBool("run-once") - httpAPI, _ := c.PersistentFlags().GetBool("http-api") +func Run(_ *cobra.Command, names []string) { + filter := filters.BuildFilter(names, c.EnableLabel, c.Scope) - if runOnce { - if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { + if c.RunOnce { + if !c.NoStartupMessage { log.Info("Running a one time update.") } runUpdatesWithNotifications(filter) @@ -151,14 +125,12 @@ func Run(c *cobra.Command, names []string) { return } - if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil { + if err := actions.CheckForMultipleWatchtowerInstances(client, c.Cleanup, c.Scope); err != nil { log.Fatal(err) } - if httpAPI { - apiToken, _ := c.PersistentFlags().GetString("http-api-token") - - if err := api.SetupHTTPUpdates(apiToken, func() { runUpdatesWithNotifications(filter) }); err != nil { + if c.HTTPAPI { + if err := api.SetupHTTPUpdates(c.HTTPAPIToken, func() { runUpdatesWithNotifications(filter) }); err != nil { log.Fatal(err) os.Exit(1) } @@ -166,20 +138,20 @@ func Run(c *cobra.Command, names []string) { api.WaitForHTTPUpdates() } - if err := runUpgradesOnSchedule(c, filter); err != nil { + if err := runUpgradesOnSchedule(filter); err != nil { log.Error(err) } os.Exit(1) } -func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { +func runUpgradesOnSchedule(filter t.Filter) error { tryLockSem := make(chan bool, 1) tryLockSem <- true - cron := cron.New() - err := cron.AddFunc( - scheduleSpec, + runner := cron.New() + err := runner.AddFunc( + viper.GetString("schedule"), func() { select { case v := <-tryLockSem: @@ -189,7 +161,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { log.Debug("Skipped another update already running.") } - nextRuns := cron.Entries() + nextRuns := runner.Entries() if len(nextRuns) > 0 { log.Debug("Scheduled next run: " + nextRuns[0].Next.String()) } @@ -199,11 +171,11 @@ 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()) + if !viper.GetBool("no-startup-message") { + log.Info("Starting Watchtower and scheduling first run: " + runner.Entries()[0].Schedule.Next(time.Now()).String()) } - cron.Start() + runner.Start() // Graceful shut-down on SIGINT/SIGTERM interrupt := make(chan os.Signal, 1) @@ -211,7 +183,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { signal.Notify(interrupt, syscall.SIGTERM) <-interrupt - cron.Stop() + runner.Stop() log.Info("Waiting for running update to be finished...") <-tryLockSem return nil @@ -221,12 +193,12 @@ func runUpdatesWithNotifications(filter t.Filter) { 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, } 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..ef0a40f --- /dev/null +++ b/internal/flags/config.go @@ -0,0 +1,31 @@ +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"` + HTTPAPI bool `mapstructure:"http-api"` + HTTPAPIToken string `mapstructure:"http-api-token"` + Timeout time.Duration `mapstructure:"stop-timeout"` + Scope string + NoColor bool `mapstructure:"no-color"` +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 2f7a89f..2e37fb6 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -8,7 +8,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -16,12 +15,15 @@ import ( // use watchtower const DockerAPIMinVersion string = "1.25" +// DefaultInterval is the default time between the start of update checks +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 +32,126 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.IntP( "interval", "i", - viper.GetInt("WATCHTOWER_POLL_INTERVAL"), + DefaultInterval, "poll interval (in seconds)") flags.StringP( "schedule", "s", - viper.GetString("WATCHTOWER_SCHEDULE"), + "", "the cron expression which defines when to update") flags.DurationP( "stop-timeout", "t", - viper.GetDuration("WATCHTOWER_TIMEOUT"), + time.Second*10, "timeout before a container is forcefully stopped") flags.BoolP( "no-pull", "", - viper.GetBool("WATCHTOWER_NO_PULL"), + false, "do not pull any new images") flags.BoolP( "no-restart", "", - viper.GetBool("WATCHTOWER_NO_RESTART"), + false, "do not restart any containers") flags.BoolP( "no-startup-message", "", - viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), + false, "Prevents watchtower from sending a startup message") flags.BoolP( "cleanup", "c", - viper.GetBool("WATCHTOWER_CLEANUP"), + false, "remove previously used images after updating") flags.BoolP( "remove-volumes", "", - viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), + false, "remove attached volumes before updating") flags.BoolP( "label-enable", "e", - viper.GetBool("WATCHTOWER_LABEL_ENABLE"), + false, "watch containers where the com.centurylinklabs.watchtower.enable label is true") flags.BoolP( "debug", "d", - viper.GetBool("WATCHTOWER_DEBUG"), + false, "enable debug mode with verbose logging") flags.BoolP( "trace", "", - viper.GetBool("WATCHTOWER_TRACE"), + false, "enable trace mode with very verbose logging - caution, exposes credentials") flags.BoolP( "monitor-only", "m", - viper.GetBool("WATCHTOWER_MONITOR_ONLY"), + false, "Will only monitor for new images, not update the containers") flags.BoolP( "run-once", "R", - viper.GetBool("WATCHTOWER_RUN_ONCE"), + false, "Run once now and exit") flags.BoolP( "include-stopped", "S", - viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), + false, "Will also include created and exited containers") flags.BoolP( "revive-stopped", "", - viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), + false, "Will also start stopped containers that were updated, if include-stopped is active") flags.BoolP( "enable-lifecycle-hooks", "", - viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), + false, "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") flags.BoolP( "rolling-restart", "", - viper.GetBool("WATCHTOWER_ROLLING_RESTART"), + false, "Restart containers one at a time") flags.BoolP( "http-api", "", - viper.GetBool("WATCHTOWER_HTTP_API"), + false, "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") flags.StringP( "http-api-token", "", - viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), + "", "Sets an authentication token to HTTP API requests.") // https://no-color.org/ flags.BoolP( "no-color", "", - viper.IsSet("NO_COLOR"), + false, "Disable ANSI color escape codes in log output") flags.StringP( "scope", "", - viper.GetString("WATCHTOWER_SCOPE"), + "", "Defines a monitoring scope for the Watchtower instance.") } @@ -160,178 +162,177 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { flags.StringSliceP( "notifications", "n", - viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), + []string{}, " notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.StringP( "notifications-level", "", - viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), + "info", "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") flags.StringP( "notification-email-from", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), + "", "Address to send notification emails from") flags.StringP( "notification-email-to", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), + "", "Address to send notification emails to") flags.IntP( "notification-email-delay", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), + 0, "Delay before sending notifications, expressed in seconds") flags.StringP( "notification-email-server", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), + "", "SMTP server to send notification emails through") flags.IntP( "notification-email-server-port", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), + 25, "SMTP server port to send notification emails through") flags.BoolP( "notification-email-server-tls-skip-verify", "", - viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"), + false, `Controls whether watchtower verifies the SMTP server's certificate chain and host name. Should only be used for testing.`) flags.StringP( "notification-email-server-user", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), + "", "SMTP server user for sending notifications") flags.StringP( "notification-email-server-password", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), + "", "SMTP server password for sending notifications") flags.StringP( "notification-email-subjecttag", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), + "", "Subject prefix tag for notifications via mail") flags.StringP( "notification-slack-hook-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), + "", "The Slack Hook URL to send notifications to") flags.StringP( "notification-slack-identifier", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), + "watchtower", "A string which will be used to identify the messages coming from this watchtower instance") flags.StringP( "notification-slack-channel", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"), + "", "A string which overrides the webhook's default channel. Example: #my-custom-channel") flags.StringP( "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( "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( "notification-msteams-hook", "", - viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"), + "", "The MSTeams WebHook URL to send notifications to") flags.BoolP( "notification-msteams-data", "", - viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), + false, "The MSTeams notifier will try to extract log entry fields as MSTeams message facts") flags.StringP( "notification-gotify-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), + "", "The Gotify URL to send notifications to") flags.StringP( "notification-gotify-token", "", - viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), + "", "The Gotify Application required to query the Gotify API") flags.BoolP( "notification-gotify-tls-skip-verify", "", - viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), + false, `Controls whether watchtower verifies the Gotify server's certificate chain and host name. Should only be used for testing.`) flags.StringP( "notification-template", "", - viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), + "", "The shoutrrr text/template for the messages") flags.StringArrayP( "notification-url", "", - viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), + []string{}, "The shoutrrr URL to send notifications to") } -// SetDefaults provides default values for environment variables -func SetDefaults() { - day := (time.Hour * 24).Seconds() +// SetEnvBindings binds environment variables to their corresponding config keys +func SetEnvBindings() { + if err := viper.BindEnv("host", "DOCKER_HOST"); err != nil { + log.Fatalf("failed to bind env DOCKER_HOST: %v", err) + } + if err := viper.BindEnv("tlsverify", "DOCKER_TLS_VERIFY"); err != nil { + log.Fatalf("failed to bind env DOCKER_TLS_VERIFY: %v", err) + } + if err := viper.BindEnv("api-version", "DOCKER_API_VERSION"); err != nil { + log.Fatalf("failed to bind env DOCKER_API_VERSION: %v", err) + } + viper.SetEnvPrefix("WATCHTOWER") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 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 } @@ -344,32 +345,6 @@ func EnvConfig(cmd *cobra.Command) error { return nil } -// 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 - - 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) - } - - return cleanup, noRestart, monitorOnly, timeout -} - func setEnvOptStr(env string, opt string) error { if opt == "" || opt == os.Getenv(env) { return nil @@ -390,9 +365,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", @@ -400,25 +373,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))) } } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index b659a96..55697f6 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,6 +1,7 @@ package flags import ( + "github.com/spf13/viper" "io/ioutil" "os" "testing" @@ -12,10 +13,11 @@ import ( func TestEnvConfig_Defaults(t *testing.T) { cmd := new(cobra.Command) - SetDefaults() RegisterDockerFlags(cmd) + SetEnvBindings() + BindViperFlags(cmd) - err := EnvConfig(cmd) + err := EnvConfig() require.NoError(t, err) assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST")) @@ -26,13 +28,14 @@ func TestEnvConfig_Defaults(t *testing.T) { func TestEnvConfig_Custom(t *testing.T) { cmd := new(cobra.Command) - SetDefaults() RegisterDockerFlags(cmd) + SetEnvBindings() + BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--host", "some-custom-docker-host", "--tlsverify", "--api-version", "1.99"}) require.NoError(t, err) - err = EnvConfig(cmd) + err = EnvConfig() require.NoError(t, err) assert.Equal(t, "some-custom-docker-host", os.Getenv("DOCKER_HOST")) @@ -71,11 +74,11 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) { func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { cmd := new(cobra.Command) - SetDefaults() RegisterNotificationFlags(cmd) - GetSecretsFromFiles(cmd) - value, err := cmd.PersistentFlags().GetString(flagName) - require.NoError(t, err) + SetEnvBindings() + BindViperFlags(cmd) + GetSecretsFromFiles() + value := viper.GetString(flagName) assert.Equal(t, expected, value) } diff --git a/pkg/container/client.go b/pkg/container/client.go index 2063332..36ea7c1 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -10,6 +10,7 @@ import ( "github.com/containrrr/watchtower/pkg/registry" "github.com/containrrr/watchtower/pkg/registry/digest" + "github.com/containrrr/watchtower/internal/flags" t "github.com/containrrr/watchtower/pkg/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -41,7 +42,7 @@ type Client interface { // * DOCKER_HOST the docker-engine host to send api requests to // * DOCKER_TLS_VERIFY whether to verify tls certificates // * DOCKER_API_VERSION the minimum docker api version to work with -func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool, includeRestarting bool) Client { +func NewClient(config *flags.WatchConfig) Client { cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) if err != nil { @@ -50,11 +51,11 @@ func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeV return dockerClient{ api: cli, - pullImages: pullImages, - removeVolumes: removeVolumes, - includeStopped: includeStopped, - reviveStopped: reviveStopped, - includeRestarting: includeRestarting, + pullImages: !config.NoPull, + removeVolumes: config.RemoveVolumes, + includeStopped: config.IncludeStopped, + reviveStopped: config.ReviveStopped, + includeRestarting: config.IncludeRestarting, } } diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 6079de7..5134178 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "github.com/spf13/cobra" + "github.com/spf13/viper" "net/smtp" "os" "strings" @@ -33,18 +34,17 @@ type emailTypeNotifier struct { delay time.Duration } -func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() +func newEmailNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - from, _ := flags.GetString("notification-email-from") - to, _ := flags.GetString("notification-email-to") - server, _ := flags.GetString("notification-email-server") - user, _ := flags.GetString("notification-email-server-user") - password, _ := flags.GetString("notification-email-server-password") - port, _ := flags.GetInt("notification-email-server-port") - tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") - delay, _ := flags.GetInt("notification-email-delay") - subjecttag, _ := flags.GetString("notification-email-subjecttag") + from := viper.GetString("notification-email-from") + to := viper.GetString("notification-email-to") + server := viper.GetString("notification-email-server") + user := viper.GetString("notification-email-server-user") + password := viper.GetString("notification-email-server-password") + port := viper.GetInt("notification-email-server-port") + tlsSkipVerify := viper.GetBool("notification-email-server-tls-skip-verify") + delay := viper.GetInt("notification-email-delay") + subjecttag := viper.GetString("notification-email-subjecttag") n := &emailTypeNotifier{ From: from, @@ -81,13 +81,13 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { // We don't use fields in watchtower, so don't bother sending them. } - t := time.Now() + now := time.Now() header := make(map[string]string) header["From"] = e.From header["To"] = e.To header["Subject"] = emailSubject - header["Date"] = t.Format(time.RFC1123Z) + header["Date"] = now.Format(time.RFC1123Z) header["MIME-Version"] = "1.0" header["Content-Type"] = "text/plain; charset=\"utf-8\"" header["Content-Transfer-Encoding"] = "base64" diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index 789f778..a86f5c0 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "github.com/spf13/viper" "net/http" "strings" @@ -24,10 +25,10 @@ type gotifyTypeNotifier struct { logLevels []log.Level } -func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() +func newGotifyNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := viper.Sub(".") - gotifyURL, _ := flags.GetString("notification-gotify-url") + gotifyURL := flags.GetString("notification-gotify-url") if len(gotifyURL) < 1 { log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.") } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) { @@ -36,12 +37,12 @@ func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifi log.Warn("Using an HTTP url for Gotify is insecure") } - gotifyToken, _ := flags.GetString("notification-gotify-token") + gotifyToken := flags.GetString("notification-gotify-token") if len(gotifyToken) < 1 { log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.") } - gotifyInsecureSkipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") + gotifyInsecureSkipVerify := flags.GetBool("notification-gotify-tls-skip-verify") n := &gotifyTypeNotifier{ gotifyURL: gotifyURL, diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index ab33966..5b96eaa 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/spf13/cobra" + "github.com/spf13/viper" "net/http" t "github.com/containrrr/watchtower/pkg/types" @@ -22,16 +23,14 @@ type msTeamsTypeNotifier struct { data bool } -func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newMsTeamsNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := cmd.PersistentFlags() - - webHookURL, _ := flags.GetString("notification-msteams-hook") + webHookURL := viper.GetString("notification-msteams-hook") if len(webHookURL) <= 0 { log.Fatal("Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.") } - withData, _ := flags.GetBool("notification-msteams-data") + withData := viper.GetBool("notification-msteams-data") n := &msTeamsTypeNotifier{ levels: acceptedLogLevels, webHookURL: webHookURL, @@ -85,19 +84,19 @@ func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { jsonBody, err := json.Marshal(webHookBody) if err != nil { - fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err) + fmt.Println("Failed to build JSON body for MSTeams notification: ", err) return } - resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) + resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer(jsonBody)) if err != nil { - fmt.Println("Failed to send MSTeams notificattion: ", err) + fmt.Println("Failed to send MSTeams notification: ", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { - fmt.Println("Failed to send MSTeams notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode) + fmt.Println("Failed to send MSTeams notification. HTTP RESPONSE STATUS: ", resp.StatusCode) if resp.Body != nil { bodyBytes, err := ioutil.ReadAll(resp.Body) if err == nil { diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index dedb21a..3e1b539 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -5,6 +5,7 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // Notifier can send log output as notification to admins, with optional batching. @@ -16,9 +17,7 @@ type Notifier struct { func NewNotifier(c *cobra.Command) *Notifier { n := &Notifier{} - f := c.PersistentFlags() - - level, _ := f.GetString("notifications-level") + level := viper.GetString("notifications-level") logLevel, err := log.ParseLevel(level) if err != nil { log.Fatalf("Notifications invalid log level: %s", err.Error()) @@ -27,7 +26,7 @@ func NewNotifier(c *cobra.Command) *Notifier { acceptedLogLevels := slackrus.LevelThreshold(logLevel) // Parse types and create notifiers. - types, err := f.GetStringSlice("notifications") + types := viper.GetStringSlice("notifications") if err != nil { log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal() } diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index d16808d..ae24ba4 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "github.com/containrrr/shoutrrr/pkg/types" + "github.com/spf13/viper" "strings" "text/template" @@ -34,9 +35,8 @@ type shoutrrrTypeNotifier struct { } func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() - urls, _ := flags.GetStringArray("notification-url") + urls := viper.GetStringSlice("notification-url") r, err := shoutrrr.CreateSender(urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) @@ -126,12 +126,11 @@ func (e *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { return nil } -func getShoutrrrTemplate(c *cobra.Command) *template.Template { +func getShoutrrrTemplate(_ *cobra.Command) *template.Template { var tpl *template.Template + var err error - flags := c.PersistentFlags() - - tplString, err := flags.GetString("notification-template") + tplString := viper.GetString("notification-template") funcs := template.FuncMap{ "ToUpper": strings.ToUpper, @@ -141,7 +140,7 @@ func getShoutrrrTemplate(c *cobra.Command) *template.Template { // If we succeed in getting a non-empty template configuration // try to parse the template string. - if tplString != "" && err == nil { + if tplString != "" { tpl, err = template.New("").Funcs(funcs).Parse(tplString) } diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index 47334af..15c1252 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -32,6 +32,7 @@ func TestShoutrrrDefaultTemplate(t *testing.T) { func TestShoutrrrTemplate(t *testing.T) { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) + flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}"}) require.NoError(t, err) @@ -55,6 +56,7 @@ func TestShoutrrrTemplate(t *testing.T) { func TestShoutrrrStringFunctions(t *testing.T) { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) + flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level | printf \"%v\" | ToUpper }}: {{.Message | ToLower }} {{.Message | Title }}{{println}}{{end}}"}) require.NoError(t, err) @@ -77,8 +79,8 @@ func TestShoutrrrStringFunctions(t *testing.T) { func TestShoutrrrInvalidTemplateUsesTemplate(t *testing.T) { cmd := new(cobra.Command) - flags.RegisterNotificationFlags(cmd) + flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{"}) require.NoError(t, err) @@ -108,7 +110,7 @@ type blockingRouter struct { sent chan bool } -func (b blockingRouter) Send(message string, params *types.Params) []error { +func (b blockingRouter) Send(_ string, _ *types.Params) []error { _ = <-b.unlock b.sent <- true return nil diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 5f96390..9129f57 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -5,6 +5,7 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) const ( @@ -15,14 +16,13 @@ type slackTypeNotifier struct { slackrus.SlackrusHook } -func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() +func newSlackNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - hookURL, _ := flags.GetString("notification-slack-hook-url") - userName, _ := flags.GetString("notification-slack-identifier") - channel, _ := flags.GetString("notification-slack-channel") - emoji, _ := flags.GetString("notification-slack-icon-emoji") - iconURL, _ := flags.GetString("notification-slack-icon-url") + hookURL := viper.GetString("notification-slack-hook-url") + userName := viper.GetString("notification-slack-identifier") + channel := viper.GetString("notification-slack-channel") + emoji := viper.GetString("notification-slack-icon-emoji") + iconURL := viper.GetString("notification-slack-icon-url") n := &slackTypeNotifier{ SlackrusHook: slackrus.SlackrusHook{