From 8b81fbd48d2fcbae853d8af582c7da9b3b391457 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 21 Dec 2020 23:08:23 +0100 Subject: [PATCH] Revert "feat(config): swap viper and cobra for config (#684)" This reverts commit ff8cb884a0852ce35a18f59802826f669c740236. --- 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, 255 insertions(+), 229 deletions(-) delete mode 100644 internal/flags/config.go diff --git a/cmd/root.go b/cmd/root.go index d0fa413..1e61308 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,10 +1,9 @@ package cmd import ( - "fmt" - "github.com/spf13/viper" "os" "os/signal" + "strconv" "syscall" "time" @@ -22,9 +21,17 @@ import ( ) var ( - client container.Client - notifier *notifications.Notifier - c flags.WatchConfig + 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 ) var rootCmd = &cobra.Command{ @@ -39,11 +46,10 @@ 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 @@ -54,10 +60,10 @@ func Execute() { } // PreRun is a lifecycle hook that runs before the command is executed. -func PreRun(cmd *cobra.Command, _ []string) { +func PreRun(cmd *cobra.Command, args []string) { + f := cmd.PersistentFlags() - // First apply all the settings that affect the output - if viper.GetBool("no-color") { + if enabled, _ := f.GetBool("no-color"); enabled { log.SetFormatter(&log.TextFormatter{ DisableColors: true, }) @@ -68,55 +74,75 @@ func PreRun(cmd *cobra.Command, _ []string) { }) } - if viper.GetBool("debug") { + if enabled, _ := f.GetBool("debug"); enabled { log.SetLevel(log.DebugLevel) } - if viper.GetBool("trace") { + if enabled, _ := f.GetBool("trace"); enabled { log.SetLevel(log.TraceLevel) } - interval := viper.GetInt("interval") + pollingSet := f.Changed("interval") + schedule, _ := f.GetString("schedule") + cronLen := len(schedule) - // 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) + 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" } - flags.GetSecretsFromFiles() + flags.GetSecretsFromFiles(cmd) + cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd) - if c.Timeout <= 0 { + if timeout < 0 { log.Fatal("Please specify a positive value for timeout value.") } - log.Debugf("Using scope %v", c.Scope) + enableLabel, _ = f.GetBool("label-enable") + lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") + rollingRestart, _ = f.GetBool("rolling-restart") + scope, _ = f.GetString("scope") + + log.Debug(scope) - if err = flags.EnvConfig(); err != nil { - log.Fatalf("failed to setup environment variables: %v", err) + // configure environment vars for client + err := flags.EnvConfig(cmd) + if err != nil { + log.Fatal(err) } - if c.MonitorOnly && c.NoPull { + 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 { 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) + client = container.NewClient( + !noPull, + includeStopped, + reviveStopped, + removeVolumes, + includeRestarting, + ) 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) +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") - if c.RunOnce { - if !c.NoStartupMessage { + if runOnce { + if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { log.Info("Running a one time update.") } runUpdatesWithNotifications(filter) @@ -125,12 +151,14 @@ func Run(_ *cobra.Command, names []string) { return } - if err := actions.CheckForMultipleWatchtowerInstances(client, c.Cleanup, c.Scope); err != nil { + if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil { log.Fatal(err) } - if c.HTTPAPI { - if err := api.SetupHTTPUpdates(c.HTTPAPIToken, func() { runUpdatesWithNotifications(filter) }); err != nil { + if httpAPI { + apiToken, _ := c.PersistentFlags().GetString("http-api-token") + + if err := api.SetupHTTPUpdates(apiToken, func() { runUpdatesWithNotifications(filter) }); err != nil { log.Fatal(err) os.Exit(1) } @@ -138,20 +166,20 @@ func Run(_ *cobra.Command, names []string) { api.WaitForHTTPUpdates() } - if err := runUpgradesOnSchedule(filter); err != nil { + if err := runUpgradesOnSchedule(c, filter); err != nil { log.Error(err) } os.Exit(1) } -func runUpgradesOnSchedule(filter t.Filter) error { +func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { tryLockSem := make(chan bool, 1) tryLockSem <- true - runner := cron.New() - err := runner.AddFunc( - viper.GetString("schedule"), + cron := cron.New() + err := cron.AddFunc( + scheduleSpec, func() { select { case v := <-tryLockSem: @@ -161,7 +189,7 @@ func runUpgradesOnSchedule(filter t.Filter) error { log.Debug("Skipped another update already running.") } - nextRuns := runner.Entries() + nextRuns := cron.Entries() if len(nextRuns) > 0 { log.Debug("Scheduled next run: " + nextRuns[0].Next.String()) } @@ -171,11 +199,11 @@ func runUpgradesOnSchedule(filter t.Filter) error { return err } - if !viper.GetBool("no-startup-message") { - log.Info("Starting Watchtower and scheduling first run: " + runner.Entries()[0].Schedule.Next(time.Now()).String()) + 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()) } - runner.Start() + cron.Start() // Graceful shut-down on SIGINT/SIGTERM interrupt := make(chan os.Signal, 1) @@ -183,7 +211,7 @@ func runUpgradesOnSchedule(filter t.Filter) error { signal.Notify(interrupt, syscall.SIGTERM) <-interrupt - runner.Stop() + cron.Stop() log.Info("Waiting for running update to be finished...") <-tryLockSem return nil @@ -193,12 +221,12 @@ 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, + Cleanup: cleanup, + NoRestart: noRestart, + Timeout: timeout, + MonitorOnly: monitorOnly, + LifecycleHooks: lifecycleHooks, + RollingRestart: rollingRestart, } err := actions.Update(client, updateParams) if err != nil { diff --git a/internal/flags/config.go b/internal/flags/config.go deleted file mode 100644 index ef0a40f..0000000 --- a/internal/flags/config.go +++ /dev/null @@ -1,31 +0,0 @@ -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 2e37fb6..2f7a89f 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -15,15 +16,12 @@ 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", "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") + 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") } // RegisterSystemFlags that are used by watchtower to modify the program flow @@ -32,126 +30,126 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.IntP( "interval", "i", - DefaultInterval, + viper.GetInt("WATCHTOWER_POLL_INTERVAL"), "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", - time.Second*10, + viper.GetDuration("WATCHTOWER_TIMEOUT"), "timeout before a container is forcefully stopped") flags.BoolP( "no-pull", "", - false, + viper.GetBool("WATCHTOWER_NO_PULL"), "do not pull any new images") flags.BoolP( "no-restart", "", - false, + viper.GetBool("WATCHTOWER_NO_RESTART"), "do not restart any containers") flags.BoolP( "no-startup-message", "", - false, + viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), "Prevents watchtower from sending a startup message") flags.BoolP( "cleanup", "c", - false, + viper.GetBool("WATCHTOWER_CLEANUP"), "remove previously used images after updating") flags.BoolP( "remove-volumes", "", - false, + viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), "remove attached volumes before updating") flags.BoolP( "label-enable", "e", - false, + viper.GetBool("WATCHTOWER_LABEL_ENABLE"), "watch containers where the com.centurylinklabs.watchtower.enable label is true") flags.BoolP( "debug", "d", - false, + viper.GetBool("WATCHTOWER_DEBUG"), "enable debug mode with verbose logging") flags.BoolP( "trace", "", - false, + viper.GetBool("WATCHTOWER_TRACE"), "enable trace mode with very verbose logging - caution, exposes credentials") flags.BoolP( "monitor-only", "m", - false, + viper.GetBool("WATCHTOWER_MONITOR_ONLY"), "Will only monitor for new images, not update the containers") flags.BoolP( "run-once", "R", - false, + viper.GetBool("WATCHTOWER_RUN_ONCE"), "Run once now and exit") flags.BoolP( "include-stopped", "S", - false, + viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), "Will also include created and exited containers") flags.BoolP( "revive-stopped", "", - false, + viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), "Will also start stopped containers that were updated, if include-stopped is active") flags.BoolP( "enable-lifecycle-hooks", "", - false, + viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") flags.BoolP( "rolling-restart", "", - false, + viper.GetBool("WATCHTOWER_ROLLING_RESTART"), "Restart containers one at a time") flags.BoolP( "http-api", "", - false, + viper.GetBool("WATCHTOWER_HTTP_API"), "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", "", - false, + viper.IsSet("NO_COLOR"), "Disable ANSI color escape codes in log output") flags.StringP( "scope", "", - "", + viper.GetString("WATCHTOWER_SCOPE"), "Defines a monitoring scope for the Watchtower instance.") } @@ -162,177 +160,178 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { flags.StringSliceP( "notifications", "n", - []string{}, + viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), " notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.StringP( "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.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", "", - 0, + viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), "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", "", - 25, + viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), "SMTP server port to send notification emails through") flags.BoolP( "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( "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", "", - "watchtower", + viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), "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", "", - 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( "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", "", - 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.StringP( "notification-template", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), "The shoutrrr text/template for the messages") flags.StringArrayP( "notification-url", "", - []string{}, + viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), "The shoutrrr URL to send notifications to") } -// 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("-", "_")) +// SetDefaults provides default values for environment variables +func SetDefaults() { + day := (time.Hour * 24).Seconds() viper.AutomaticEnv() -} - -// 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) - } + 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") } // EnvConfig translates the command-line options into environment variables // that will initialize the api client -func EnvConfig() error { +func EnvConfig(cmd *cobra.Command) error { var err error + var host string var tls bool var version string - host := viper.GetString("host") - tls = viper.GetBool("tlsverify") - version = viper.GetString("api-version") + 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 + } if err = setEnvOptStr("DOCKER_HOST", host); err != nil { return err } @@ -345,6 +344,32 @@ func EnvConfig() 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 @@ -365,7 +390,9 @@ 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() { +func GetSecretsFromFiles(rootCmd *cobra.Command) { + flags := rootCmd.PersistentFlags() + secrets := []string{ "notification-email-server-password", "notification-slack-hook-url", @@ -373,19 +400,25 @@ func GetSecretsFromFiles() { "notification-gotify-token", } for _, secret := range secrets { - getSecretFromFile(secret) + getSecretFromFile(flags, 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(secret string) { - value := viper.GetString(secret) +func getSecretFromFile(flags *pflag.FlagSet, secret string) { + value, err := flags.GetString(secret) + if err != nil { + log.Error(err) + } if value != "" && isFile(value) { file, err := ioutil.ReadFile(value) if err != nil { log.Fatal(err) } - viper.Set(secret, strings.TrimSpace(string(file))) + err = flags.Set(secret, strings.TrimSpace(string(file))) + if err != nil { + log.Error(err) + } } } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 55697f6..b659a96 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,7 +1,6 @@ package flags import ( - "github.com/spf13/viper" "io/ioutil" "os" "testing" @@ -13,11 +12,10 @@ import ( func TestEnvConfig_Defaults(t *testing.T) { cmd := new(cobra.Command) + SetDefaults() RegisterDockerFlags(cmd) - SetEnvBindings() - BindViperFlags(cmd) - err := EnvConfig() + err := EnvConfig(cmd) require.NoError(t, err) assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST")) @@ -28,14 +26,13 @@ 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() + err = EnvConfig(cmd) require.NoError(t, err) assert.Equal(t, "some-custom-docker-host", os.Getenv("DOCKER_HOST")) @@ -74,11 +71,11 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) { func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { cmd := new(cobra.Command) + SetDefaults() RegisterNotificationFlags(cmd) - SetEnvBindings() - BindViperFlags(cmd) - GetSecretsFromFiles() - value := viper.GetString(flagName) + GetSecretsFromFiles(cmd) + value, err := cmd.PersistentFlags().GetString(flagName) + require.NoError(t, err) assert.Equal(t, expected, value) } diff --git a/pkg/container/client.go b/pkg/container/client.go index 36ea7c1..2063332 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -10,7 +10,6 @@ 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" @@ -42,7 +41,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(config *flags.WatchConfig) Client { +func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool, includeRestarting bool) Client { cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) if err != nil { @@ -51,11 +50,11 @@ func NewClient(config *flags.WatchConfig) Client { return dockerClient{ api: cli, - pullImages: !config.NoPull, - removeVolumes: config.RemoveVolumes, - includeStopped: config.IncludeStopped, - reviveStopped: config.ReviveStopped, - includeRestarting: config.IncludeRestarting, + pullImages: pullImages, + removeVolumes: removeVolumes, + includeStopped: includeStopped, + reviveStopped: reviveStopped, + includeRestarting: includeRestarting, } } diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 5134178..6079de7 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "fmt" "github.com/spf13/cobra" - "github.com/spf13/viper" "net/smtp" "os" "strings" @@ -34,17 +33,18 @@ type emailTypeNotifier struct { delay time.Duration } -func newEmailNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - 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") + 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") 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. } - now := time.Now() + t := time.Now() header := make(map[string]string) header["From"] = e.From header["To"] = e.To header["Subject"] = emailSubject - header["Date"] = now.Format(time.RFC1123Z) + header["Date"] = t.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 a86f5c0..789f778 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "encoding/json" "fmt" - "github.com/spf13/viper" "net/http" "strings" @@ -25,10 +24,10 @@ type gotifyTypeNotifier struct { logLevels []log.Level } -func newGotifyNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := viper.Sub(".") +func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - 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://")) { @@ -37,12 +36,12 @@ func newGotifyNotifier(_ *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 5b96eaa..ab33966 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "github.com/spf13/cobra" - "github.com/spf13/viper" "net/http" t "github.com/containrrr/watchtower/pkg/types" @@ -23,14 +22,16 @@ type msTeamsTypeNotifier struct { data bool } -func newMsTeamsNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - webHookURL := viper.GetString("notification-msteams-hook") + flags := cmd.PersistentFlags() + + webHookURL, _ := flags.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 := viper.GetBool("notification-msteams-data") + withData, _ := flags.GetBool("notification-msteams-data") n := &msTeamsTypeNotifier{ levels: acceptedLogLevels, webHookURL: webHookURL, @@ -84,19 +85,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 notification: ", err) + fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err) return } - resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer(jsonBody)) + resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) if err != nil { - fmt.Println("Failed to send MSTeams notification: ", err) + fmt.Println("Failed to send MSTeams notificattion: ", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { - fmt.Println("Failed to send MSTeams notification. HTTP RESPONSE STATUS: ", resp.StatusCode) + fmt.Println("Failed to send MSTeams notificattion. 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 3e1b539..dedb21a 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -5,7 +5,6 @@ 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. @@ -17,7 +16,9 @@ type Notifier struct { func NewNotifier(c *cobra.Command) *Notifier { n := &Notifier{} - level := viper.GetString("notifications-level") + f := c.PersistentFlags() + + level, _ := f.GetString("notifications-level") logLevel, err := log.ParseLevel(level) if err != nil { log.Fatalf("Notifications invalid log level: %s", err.Error()) @@ -26,7 +27,7 @@ func NewNotifier(c *cobra.Command) *Notifier { acceptedLogLevels := slackrus.LevelThreshold(logLevel) // Parse types and create notifiers. - types := viper.GetStringSlice("notifications") + types, err := f.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 ae24ba4..d16808d 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "github.com/containrrr/shoutrrr/pkg/types" - "github.com/spf13/viper" "strings" "text/template" @@ -35,8 +34,9 @@ type shoutrrrTypeNotifier struct { } func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - urls := viper.GetStringSlice("notification-url") + urls, _ := flags.GetStringArray("notification-url") r, err := shoutrrr.CreateSender(urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) @@ -126,11 +126,12 @@ func (e *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { return nil } -func getShoutrrrTemplate(_ *cobra.Command) *template.Template { +func getShoutrrrTemplate(c *cobra.Command) *template.Template { var tpl *template.Template - var err error - tplString := viper.GetString("notification-template") + flags := c.PersistentFlags() + + tplString, err := flags.GetString("notification-template") funcs := template.FuncMap{ "ToUpper": strings.ToUpper, @@ -140,7 +141,7 @@ func getShoutrrrTemplate(_ *cobra.Command) *template.Template { // If we succeed in getting a non-empty template configuration // try to parse the template string. - if tplString != "" { + if tplString != "" && err == nil { tpl, err = template.New("").Funcs(funcs).Parse(tplString) } diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index 15c1252..47334af 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -32,7 +32,6 @@ 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) @@ -56,7 +55,6 @@ 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) @@ -79,8 +77,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) @@ -110,7 +108,7 @@ type blockingRouter struct { sent chan bool } -func (b blockingRouter) Send(_ string, _ *types.Params) []error { +func (b blockingRouter) Send(message string, params *types.Params) []error { _ = <-b.unlock b.sent <- true return nil diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 9129f57..5f96390 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -5,7 +5,6 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/viper" ) const ( @@ -16,13 +15,14 @@ type slackTypeNotifier struct { slackrus.SlackrusHook } -func newSlackNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - 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") + 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") n := &slackTypeNotifier{ SlackrusHook: slackrus.SlackrusHook{