diff --git a/cmd/root.go b/cmd/root.go index 116e9d9..cd59e42 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/containrrr/watchtower/internal/config" "math" "os" "os/signal" @@ -11,7 +12,6 @@ import ( "time" "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" @@ -31,7 +31,7 @@ import ( var ( client container.Client notifier t.Notifier - c flags.WatchConfig + c config.WatchConfig ) var rootCmd = NewRootCommand() @@ -51,11 +51,10 @@ func NewRootCommand() *cobra.Command { } func init() { - flags.RegisterDockerFlags(rootCmd) - flags.RegisterSystemFlags(rootCmd) - flags.RegisterNotificationFlags(rootCmd) - flags.SetEnvBindings() - flags.BindViperFlags(rootCmd) + config.RegisterDockerOptions(rootCmd) + config.RegisterSystemOptions(rootCmd) + config.RegisterNotificationOptions(rootCmd) + config.BindViperFlags(rootCmd) } // Execute the root func and exit in case of errors @@ -69,7 +68,7 @@ func Execute() { func PreRun(_ *cobra.Command, _ []string) { // First apply all the settings that affect the output - if viper.GetBool("no-color") { + if config.GetBool(config.NoColor) { log.SetFormatter(&log.TextFormatter{ DisableColors: true, }) @@ -80,19 +79,19 @@ func PreRun(_ *cobra.Command, _ []string) { }) } - if viper.GetBool("debug") { + if config.GetBool(config.Debug) { log.SetLevel(log.DebugLevel) } - if viper.GetBool("trace") { + if config.GetBool(config.Trace) { log.SetLevel(log.TraceLevel) } - interval := viper.GetInt("interval") + interval := config.GetInt(config.Interval) // If empty, set schedule using interval helper value - if viper.GetString("schedule") == "" { - viper.Set("schedule", fmt.Sprintf("@every %ds", interval)) - } else if interval != flags.DefaultInterval { + if config.GetString(config.Schedule) == "" { + viper.Set(string(config.Schedule), fmt.Sprintf("@every %ds", interval)) + } else if interval != config.DefaultInterval { log.Fatal("only schedule or interval can be defined, not both") } @@ -102,7 +101,7 @@ func PreRun(_ *cobra.Command, _ []string) { log.Fatalf("unable to decode into struct, %v", err) } - flags.GetSecretsFromFiles() + config.GetSecretsFromFiles() if c.Timeout <= 0 { log.Fatal("Please specify a positive value for timeout value.") @@ -110,7 +109,7 @@ func PreRun(_ *cobra.Command, _ []string) { log.Debugf("Using scope %v", c.Scope) - if err = flags.EnvConfig(); err != nil { + if err = config.EnvConfig(); err != nil { log.Fatalf("failed to setup environment variables: %v", err) } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..e9bcfcd --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,89 @@ +package config + +import ( + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "io/ioutil" + "os" + "strings" +) + +// 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() error { + var err error + + host := GetString(DockerHost) + tls := GetBool(DockerTlSVerify) + version := GetString(DockerApiVersion) + if err = setEnvOptStr("DOCKER_HOST", host); err != nil { + return err + } + if err = setEnvOptBool("DOCKER_TLS_VERIFY", tls); err != nil { + return err + } + if err = setEnvOptStr("DOCKER_API_VERSION", version); err != nil { + return err + } + return nil +} + +func setEnvOptStr(env string, opt string) error { + if opt == "" || opt == os.Getenv(env) { + return nil + } + err := os.Setenv(env, opt) + if err != nil { + return err + } + return nil +} + +func setEnvOptBool(env string, opt bool) error { + if opt { + return setEnvOptStr(env, "1") + } + return nil +} + +// 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() { + secrets := []string{ + string(NotificationEmailServerPassword), + string(NotificationSlackHookUrl), + string(NotificationMsteamsHook), + string(NotificationGotifyToken), + } + for _, secret := range secrets { + 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(secret string) { + value := viper.GetString(secret) + if value != "" && isFile(value) { + file, err := ioutil.ReadFile(value) + if err != nil { + log.Fatal(err) + } + viper.Set(secret, strings.TrimSpace(string(file))) + } +} + +func isFile(s string) bool { + _, err := os.Stat(s) + if os.IsNotExist(err) { + return false + } + return true +} diff --git a/internal/flags/flags_test.go b/internal/config/config_test.go similarity index 74% rename from internal/flags/flags_test.go rename to internal/config/config_test.go index 9beb1da..5d3a78a 100644 --- a/internal/flags/flags_test.go +++ b/internal/config/config_test.go @@ -1,4 +1,4 @@ -package flags +package config import ( "io/ioutil" @@ -13,7 +13,7 @@ import ( func TestEnvConfig_Defaults(t *testing.T) { cmd := new(cobra.Command) - RegisterDockerFlags(cmd) + RegisterDockerOptions(cmd) BindViperFlags(cmd) err := EnvConfig() @@ -27,7 +27,7 @@ func TestEnvConfig_Defaults(t *testing.T) { func TestEnvConfig_Custom(t *testing.T) { cmd := new(cobra.Command) - RegisterDockerFlags(cmd) + RegisterDockerOptions(cmd) BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--host", "some-custom-docker-host", "--tlsverify", "--api-version", "1.99"}) @@ -75,9 +75,9 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) { func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { cmd := new(cobra.Command) - RegisterNotificationFlags(cmd) + RegisterNotificationOptions(cmd) BindViperFlags(cmd) - SetEnvBindings() + GetSecretsFromFiles() value := viper.GetString(flagName) @@ -87,8 +87,8 @@ func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { func TestHTTPAPIPeriodicPollsFlag(t *testing.T) { cmd := new(cobra.Command) - RegisterDockerFlags(cmd) - RegisterSystemFlags(cmd) + RegisterDockerOptions(cmd) + RegisterSystemOptions(cmd) err := cmd.ParseFlags([]string{"--http-api-periodic-polls"}) require.NoError(t, err) @@ -98,3 +98,32 @@ func TestHTTPAPIPeriodicPollsFlag(t *testing.T) { assert.Equal(t, true, periodicPolls) } + +func TestEnvVariablesMapToFlags(t *testing.T) { + + viper.Reset() + cmd := new(cobra.Command) + + RegisterDockerOptions(cmd) + RegisterSystemOptions(cmd) + RegisterNotificationOptions(cmd) + BindViperFlags(cmd) + + //for _, opt := range stringConfOpts { + // value := opt.key + // assert.Nil(t, os.Setenv(opt.env, value)) + // assert.Equal(t, value, viper.GetString(opt.key)) + //} + // + //for _, opt := range intConfOpts { + // value := len(opt.key) + // assert.Nil(t, os.Setenv(opt.env, fmt.Sprint(value))) + // assert.Equal(t, value, viper.GetInt(opt.key)) + //} + // + //for _, opt := range boolConfOpts { + // assert.Nil(t, os.Setenv(opt.env, fmt.Sprint(true))) + // assert.True(t, viper.GetBool(opt.key)) + //} + +} diff --git a/internal/flags/flaghelper.go b/internal/config/flaghelper.go similarity index 99% rename from internal/flags/flaghelper.go rename to internal/config/flaghelper.go index 75ed5d1..800c48c 100644 --- a/internal/flags/flaghelper.go +++ b/internal/config/flaghelper.go @@ -1,4 +1,4 @@ -package flags +package config import ( "github.com/spf13/pflag" diff --git a/internal/config/keys.go b/internal/config/keys.go new file mode 100644 index 0000000..3b8e06a --- /dev/null +++ b/internal/config/keys.go @@ -0,0 +1,78 @@ +package config + +type stringConfKey string +type boolConfKey string +type intConfKey string +type durationConfKey string +type sliceConfKey string + +// +const ( + NoPull boolConfKey = "no-pull" + NoRestart boolConfKey = "no-restart" + NoStartupMessage boolConfKey = "no-startup-message" + Cleanup boolConfKey = "cleanup" + RemoveVolumes boolConfKey = "remove-volumes" + LabelEnable boolConfKey = "label-enable" + Debug boolConfKey = "debug" + Trace boolConfKey = "trace" + MonitorOnly boolConfKey = "monitor-only" + RunOnce boolConfKey = "run-once" + IncludeRestarting boolConfKey = "include-restarting" + IncludeStopped boolConfKey = "include-stopped" + ReviveStopped boolConfKey = "revive-stopped" + EnableLifecycleHooks boolConfKey = "enable-lifecycle-hooks" + RollingRestart boolConfKey = "rolling-restart" + WarnOnHeadFailure stringConfKey = "warn-on-head-failure" + + HttpApiUpdate boolConfKey = "http-api-update" + HttpApiMetrics boolConfKey = "http-api-metrics" + HttpApiPeriodicPolls boolConfKey = "http-api-periodic-polls" + HttpApiToken stringConfKey = "HttpApiToken" + + NoColor boolConfKey = "no-color" + + NotificationGotifyTlsSkipVerify boolConfKey = "notification-gotify-tls-skip-verify" + + Schedule stringConfKey = "schedule" + Interval intConfKey = "interval" + + StopTimeout durationConfKey = "stop-timeout" + + Scope stringConfKey = "Scope" + + /* Docker v*/ + DockerHost stringConfKey = "host" + DockerApiVersion stringConfKey = "api-version" + DockerTlSVerify boolConfKey = "tlsverify" + + Notifications sliceConfKey = "notifications" + NotificationsLevel stringConfKey = "notifications-level" + NotificationsDelay intConfKey = "notifications-delay" + NotificationsHostname stringConfKey = "notifications-hostname" + NotificationTemplate stringConfKey = "notification-template" + NotificationReport boolConfKey = "notification-report" + NotificationUrl sliceConfKey = "notification-url" + + NotificationEmailFrom stringConfKey = "notification-email-from" + NotificationEmailTo stringConfKey = "notification-email-to" + NotificationEmailServer stringConfKey = "notification-email-server" + NotificationEmailServerUser stringConfKey = "notification-email-server-user" + NotificationEmailServerPassword stringConfKey = "notification-email-server-password" + NotificationEmailSubjecttag stringConfKey = "notification-email-subjecttag" + NotificationEmailDelay intConfKey = "notification-email-delay" + NotificationEmailServerPort intConfKey = "notification-email-server-port" + NotificationEmailServerTlsSkipVerify boolConfKey = "notification-email-server-tls-skip-verify" + + NotificationSlackHookUrl stringConfKey = "notification-slack-hook-url" + NotificationSlackIdentifier stringConfKey = "notification-slack-identifier" + NotificationSlackChannel stringConfKey = "notification-slack-channel" + NotificationSlackIconEmoji stringConfKey = "notification-slack-icon-emoji" + NotificationSlackIconUrl stringConfKey = "notification-slack-icon-url" + + NotificationMsteamsHook stringConfKey = "notification-msteams-hook" + NotificationMsteamsData boolConfKey = "notification-msteams-data" + + NotificationGotifyUrl stringConfKey = "notification-gotify-url" + NotificationGotifyToken stringConfKey = "notification-gotify-token" +) diff --git a/internal/config/legacy.go b/internal/config/legacy.go new file mode 100644 index 0000000..00bc00f --- /dev/null +++ b/internal/config/legacy.go @@ -0,0 +1,86 @@ +package config + +import ( + "github.com/spf13/pflag" +) + +// RegisterLegacyNotificationFlags registers all the flags related to the old notification system +func RegisterLegacyNotificationFlags(flags *pflag.FlagSet) { + ob := OptBuilder(flags) + // Hide all legacy notification flags from the `--help` to reduce clutter + ob.Hide = true + + ob.String(NotificationEmailFrom, "", + "Address to send notification emails from", "WATCHTOWER_NOTIFICATION_EMAIL_FROM") + + ob.String(NotificationEmailTo, "", + "Address to send notification emails to", "WATCHTOWER_NOTIFICATION_EMAIL_TO") + + ob.Int(NotificationEmailDelay, 0, + "Delay before sending notifications, expressed in seconds", "WATCHTOWER_NOTIFICATION_EMAIL_DELAY") + _ = ob.Flags.MarkDeprecated(string(NotificationEmailDelay), + "use "+string(NotificationsDelay)+" instead") + + ob.String(NotificationEmailServer, "", + "SMTP server to send notification emails through", "WATCHTOWER_NOTIFICATION_EMAIL_SERVER") + + ob.Int(NotificationEmailServerPort, 25, + "SMTP server port to send notification emails through", "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT") + + ob.Bool(NotificationEmailServerTlsSkipVerify, false, + `Controls whether watchtower verifies the SMTP server's certificate chain and host name. +Should only be used for testing.`, + "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY") + + ob.String(NotificationEmailServerUser, "", + "SMTP server user for sending notifications", + "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER") + + ob.String(NotificationEmailServerPassword, "", + "SMTP server password for sending notifications", + "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD") + + ob.String(NotificationEmailSubjecttag, "", + "Subject prefix tag for notifications via mail", + "WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG") + + ob.String(NotificationSlackHookUrl, "", + "The Slack Hook URL to send notifications to", + "WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL") + + ob.String(NotificationSlackIdentifier, "watchtower", + "A string which will be used to identify the messages coming from this watchtower instance", + "WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER") + + ob.String(NotificationSlackChannel, "", + "A string which overrides the webhook's default channel. Example: #my-custom-channel", + "WATCHTOWER_NOTIFICATION_SLACK_CHANNEL") + + ob.String(NotificationSlackIconEmoji, "", + "An emoji code string to use in place of the default icon", + "WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI") + + ob.String(NotificationSlackIconUrl, "", + "An icon image URL string to use in place of the default icon", + "WATCHTOWER_NOTIFICATION_SLACK_ICON_URL") + + ob.String(NotificationMsteamsHook, "", + "The MSTeams WebHook URL to send notifications to", + "WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL") + + ob.Bool(NotificationMsteamsData, false, + "The MSTeams notifier will try to extract log entry fields as MSTeams message facts", + "WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA") + + ob.String(NotificationGotifyUrl, "", + "The Gotify URL to send notifications to", "WATCHTOWER_NOTIFICATION_GOTIFY_URL") + + ob.String(NotificationGotifyToken, "", + "The Gotify Application required to query the Gotify API", "WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN") + + ob.Bool(NotificationGotifyTlsSkipVerify, false, + `Controls whether watchtower verifies the Gotify server's certificate chain and host name. +Should only be used for testing.`, + "WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY") + +} diff --git a/internal/config/options.go b/internal/config/options.go new file mode 100644 index 0000000..785b4ce --- /dev/null +++ b/internal/config/options.go @@ -0,0 +1,87 @@ +package config + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + "time" +) + +type optBuilder struct { + Flags *pflag.FlagSet + Hide bool +} + +func OptBuilder(flags *pflag.FlagSet) *optBuilder { + return &optBuilder{ + Flags: flags, + } +} + +func (ob *optBuilder) register(key string, env string) { + _ = viper.BindEnv(key, env) + if ob.Hide { + _ = ob.Flags.MarkHidden(key) + } +} + +func (ob *optBuilder) StringP(key stringConfKey, short string, defaultValue string, usage string, env string) { + ob.Flags.StringP(string(key), short, defaultValue, usage) + ob.register(string(key), env) +} + +func (ob *optBuilder) BoolP(key boolConfKey, short string, defaultValue bool, usage string, env string) { + ob.Flags.BoolP(string(key), short, defaultValue, usage) + ob.register(string(key), env) +} + +func (ob *optBuilder) IntP(key intConfKey, short string, defaultValue int, usage string, env string) { + ob.Flags.IntP(string(key), short, defaultValue, usage) + ob.register(string(key), env) +} + +func (ob *optBuilder) DurationP(key durationConfKey, short string, defaultValue time.Duration, usage string, env string) { + ob.Flags.DurationP(string(key), short, defaultValue, usage) + ob.register(string(key), env) +} + +func (ob *optBuilder) String(key stringConfKey, defaultValue string, usage string, env string) { + ob.StringP(key, "", defaultValue, usage, env) +} + +func (ob *optBuilder) Bool(key boolConfKey, defaultValue bool, usage string, env string) { + ob.BoolP(key, "", defaultValue, usage, env) +} + +func (ob *optBuilder) Int(key intConfKey, defaultValue int, usage string, env string) { + ob.IntP(key, "", defaultValue, usage, env) +} + +func (ob *optBuilder) StringArray(key sliceConfKey, defaultValue []string, usage string, env string) { + ob.Flags.StringArray(string(key), defaultValue, usage) + ob.register(string(key), env) +} + +func (ob *optBuilder) StringSliceP(key sliceConfKey, short string, defaultValue []string, usage string, env string) { + ob.Flags.StringSliceP(string(key), short, defaultValue, usage) + ob.register(string(key), env) +} + +func GetString(key stringConfKey) string { + return viper.GetString(string(key)) +} + +func GetBool(key boolConfKey) bool { + return viper.GetBool(string(key)) +} + +func GetInt(key intConfKey) int { + return viper.GetInt(string(key)) +} + +func GetDuration(key durationConfKey) time.Duration { + return viper.GetDuration(string(key)) +} + +func GetSlice(key sliceConfKey) []string { + return viper.GetStringSlice(string(key)) +} diff --git a/internal/config/reg_options.go b/internal/config/reg_options.go new file mode 100644 index 0000000..349697c --- /dev/null +++ b/internal/config/reg_options.go @@ -0,0 +1,179 @@ +package config + +import ( + "time" + + "github.com/spf13/cobra" +) + +// DockerAPIMinVersion is the minimum version of the docker api required to +// 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) + +// RegisterDockerOptions that are used directly by the docker api client +func RegisterDockerOptions(rootCmd *cobra.Command) { + ob := OptBuilder(rootCmd.PersistentFlags()) + + ob.StringP(DockerHost, "H", "unix:///var/run/docker.sock", + "daemon socket to connect to", + "DOCKER_HOST") + + ob.BoolP(DockerTlSVerify, "v", false, + "use TLS and verify the remote", + "DOCKER_TLS_VERIFY") + + ob.StringP(DockerApiVersion, "a", DockerAPIMinVersion, + "api version to use by docker client", + "DOCKER_API_VERSION") +} + +// RegisterSystemOptions that are used by watchtower to modify the program flow +func RegisterSystemOptions(rootCmd *cobra.Command) { + ob := OptBuilder(rootCmd.PersistentFlags()) + + ob.IntP(Interval, "i", DefaultInterval, + "poll interval (in seconds)", + "WATCHTOWER_POLL_INTERVAL") + + ob.StringP(Schedule, "s", "", + "The cron expression which defines when to update", + "WATCHTOWER_SCHEDULE") + + ob.DurationP(StopTimeout, "t", time.Second*10, + "Timeout before a container is forcefully stopped", + "WATCHTOWER_TIMEOUT") + + ob.Bool(NoPull, false, + "Do not pull any new images", + "WATCHTOWER_NO_PULL") + + ob.Bool(NoRestart, false, + "Do not restart any containers", + "WATCHTOWER_NO_RESTART") + + ob.Bool(NoStartupMessage, false, + "Prevents watchtower from sending a startup message", + "WATCHTOWER_NO_STARTUP_MESSAGE") + + ob.BoolP(Cleanup, "c", false, + "Remove previously used images after updating", + "WATCHTOWER_CLEANUP") + + ob.BoolP(RemoveVolumes, + "", + false, + "Remove attached volumes before updating", + "WATCHTOWER_REMOVE_VOLUMES") + + ob.BoolP(LabelEnable, + "e", + false, + "Watch containers where the com.centurylinklabs.watchtower.enable label is true", + "WATCHTOWER_LABEL_ENABLE") + + ob.BoolP(Debug, + "d", + false, + "Enable debug mode with verbose logging", + "WATCHTOWER_DEBUG") + + ob.Bool(Trace, + false, + "Enable trace mode with very verbose logging - caution, exposes credentials", + "WATCHTOWER_TRACE") + + ob.BoolP(MonitorOnly, "m", false, + "Will only monitor for new images, not update the containers", + "WATCHTOWER_MONITOR_ONLY") + + ob.BoolP(RunOnce, "R", false, + "Run once now and exit", + "WATCHTOWER_RUN_ONCE") + + ob.BoolP(IncludeRestarting, "", false, + "Will also include restarting containers", + "WATCHTOWER_INCLUDE_RESTARTING") + + ob.BoolP(IncludeStopped, "S", false, + "Will also include created and exited containers", + "WATCHTOWER_INCLUDE_STOPPED") + + ob.Bool(ReviveStopped, false, + "Will also start stopped containers that were updated, if include-stopped is active", + "WATCHTOWER_REVIVE_STOPPED") + + ob.Bool(EnableLifecycleHooks, false, + "Enable the execution of commands triggered by pre- and post-update lifecycle hooks", + "WATCHTOWER_LIFECYCLE_HOOKS") + + ob.Bool(RollingRestart, false, + "Restart containers one at a time", + "WATCHTOWER_ROLLING_RESTART") + + ob.Bool(HttpApiUpdate, false, + "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request", + "WATCHTOWER_HTTP_API_UPDATE") + + ob.Bool(HttpApiMetrics, false, + "Runs Watchtower with the Prometheus metrics API enabled", + "WATCHTOWER_HTTP_API_METRICS") + + ob.String(HttpApiToken, "", + "Sets an authentication token to HTTP API requests.", + "WATCHTOWER_HTTP_API_TOKEN") + + ob.Bool(HttpApiPeriodicPolls, false, + "Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled", + "WATCHTOWER_HTTP_API_PERIODIC_POLLS") + + // https://no-color.org/ + ob.Bool(NoColor, false, + "Disable ANSI color escape codes in log output", + "NO_COLOR") + + ob.String(Scope, "", + "Defines a monitoring scope for the Watchtower instance.", + "WATCHTOWER_SCOPE") +} + +// RegisterNotificationOptions that are used by watchtower to send notifications +func RegisterNotificationOptions(cmd *cobra.Command) { + ob := OptBuilder(cmd.PersistentFlags()) + + ob.StringSliceP(Notifications, "n", []string{}, + " Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)", + "WATCHTOWER_NOTIFICATIONS") + + ob.String(NotificationsLevel, "info", + "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug", + "WATCHTOWER_NOTIFICATIONS_LEVEL") + + ob.Int(NotificationsDelay, 0, + "Delay before sending notifications, expressed in seconds", + "WATCHTOWER_NOTIFICATIONS_DELAY") + + ob.String(NotificationsHostname, "", + "Custom hostname for notification titles", + "WATCHTOWER_NOTIFICATIONS_HOSTNAME") + + ob.String(NotificationTemplate, "", + "The shoutrrr text/template for the messages", + "WATCHTOWER_NOTIFICATION_TEMPLATE") + + ob.StringArray(NotificationUrl, []string{}, + "The shoutrrr URL to send notifications to", + "WATCHTOWER_NOTIFICATION_URL") + + ob.Bool(NotificationReport, false, + "Use the session report as the notification template data", + "WATCHTOWER_NOTIFICATION_REPORT") + + ob.String(WarnOnHeadFailure, "auto", + "When to warn about HEAD pull requests failing. Possible values: always, auto or never", + "WATCHTOWER_WARN_ON_HEAD_FAILURE") + + RegisterLegacyNotificationFlags(ob.Flags) +} diff --git a/internal/flags/config.go b/internal/config/watchconfig.go similarity index 57% rename from internal/flags/config.go rename to internal/config/watchconfig.go index 4c3bf49..351b8eb 100644 --- a/internal/flags/config.go +++ b/internal/config/watchconfig.go @@ -1,4 +1,4 @@ -package flags +package config import ( "time" @@ -8,12 +8,12 @@ import ( type WatchConfig struct { Interval int Schedule string - NoPull bool `mapstructure:"no-pull"` - NoRestart bool `mapstructure:"no-restart"` - NoStartupMessage bool `mapstructure:"no-startup-message"` + 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"` + RemoveVolumes bool `mapstructure:"remove-volumes"` + EnableLabel bool `mapstructure:"label-enable"` Debug bool Trace bool MonitorOnly bool `mapstructure:"monitor-only"` @@ -25,10 +25,10 @@ type WatchConfig struct { RollingRestart bool `mapstructure:"rolling-restart"` HTTPAPIToken string `mapstructure:"http-api-token"` 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"` + 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"` Timeout time.Duration `mapstructure:"stop-timeout"` } diff --git a/internal/flags/deprecated.go b/internal/flags/deprecated.go deleted file mode 100644 index 78ffbe7..0000000 --- a/internal/flags/deprecated.go +++ /dev/null @@ -1,77 +0,0 @@ -package flags - -import "github.com/spf13/pflag" - -// RegisterLegacyNotificationFlags registers all the flags related to the old notification system -func RegisterLegacyNotificationFlags(flags *pflag.FlagSet) { - depFlags := NewDeprecator(flags, "use notification-url instead") - depFlags.Deprecate = false - - depFlags.Prefix = "notification-email-" - - // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), - depFlags.String("from", "", "Address to send notification emails from") - - // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), - depFlags.String("to", "", "Address to send notification emails to") - - //viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), - depFlags.Int("delay", 0, "Delay before sending notifications, expressed in seconds") - - // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), - depFlags.String("server", "", "SMTP server to send notification emails through") - - // viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), - depFlags.Int("server-port", 25, "SMTP server port to send notification emails through") - - // viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"), - depFlags.Bool("server-tls-skip-verify", false, `Controls whether watchtower verifies the SMTP server's certificate chain and host name. -Should only be used for testing.`) - - // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), - depFlags.String("server-user", "", "SMTP server user for sending notifications") - - // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), - depFlags.String("server-password", "", "SMTP server password for sending notifications") - - // viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), - depFlags.String("subjecttag", "", "Subject prefix tag for notifications via mail") - - depFlags.Prefix = "notification-slack-" - - // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), - depFlags.String("hook-url", "", "The Slack Hook URL to send notifications to") - - // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), - depFlags.String("identifier", "watchtower", "A string which will be used to identify the messages coming from this watchtower instance") - - // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"), - depFlags.String("channel", "", "A string which overrides the webhook's default channel. Example: #my-custom-channel") - - // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"), - depFlags.String("icon-emoji", "", "An emoji code string to use in place of the default icon") - - // viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"), - depFlags.String("icon-url", "", "An icon image URL string to use in place of the default icon") - - depFlags.Prefix = "notification-msteams-" - - // viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"), - depFlags.String("hook", "", "The MSTeams WebHook URL to send notifications to") - - // viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), - depFlags.Bool("data", false, "The MSTeams notifier will try to extract log entry fields as MSTeams message facts") - - depFlags.Prefix = "notification-gotify-" - - // viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), - depFlags.String("url", "", "The Gotify URL to send notifications to") - - // viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), - depFlags.String("token", "", "The Gotify Application required to query the Gotify API") - - // viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), - depFlags.Bool("tls-skip-verify", false, `Controls whether watchtower verifies the Gotify server's certificate chain and host name. -Should only be used for testing.`) - -} diff --git a/internal/flags/flags.go b/internal/flags/flags.go deleted file mode 100644 index 6488454..0000000 --- a/internal/flags/flags.go +++ /dev/null @@ -1,330 +0,0 @@ -package flags - -import ( - "io/ioutil" - "os" - "strings" - "time" - - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// DockerAPIMinVersion is the minimum version of the docker api required to -// 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") -} - -// RegisterSystemFlags that are used by watchtower to modify the program flow -func RegisterSystemFlags(rootCmd *cobra.Command) { - flags := rootCmd.PersistentFlags() - flags.IntP( - "interval", - "i", - DefaultInterval, // viper.GetInt("WATCHTOWER_POLL_INTERVAL"), - "poll interval (in seconds)") - - flags.StringP( - "schedule", - "s", - "", - "The cron expression which defines when to update") - //viper.GetString("WATCHTOWER_SCHEDULE"), - - 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.Bool( - "no-restart", - false, // viper.GetBool("WATCHTOWER_NO_RESTART"), - "Do not restart any containers") - - flags.Bool( - "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.Bool( - "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-restarting", - "", - false, // viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"), - "Will also include restarting containers") - - flags.BoolP( - "include-stopped", - "S", - false, // viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), - "Will also include created and exited containers") - - flags.Bool( - "revive-stopped", - false, // viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), - "Will also start stopped containers that were updated, if include-stopped is active") - - flags.Bool( - "enable-lifecycle-hooks", - false, // viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), - "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") - - flags.Bool( - "rolling-restart", - false, // viper.GetBool("WATCHTOWER_ROLLING_RESTART"), - "Restart containers one at a time") - - flags.Bool( - "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.Bool( - "http-api-metrics", - false, // viper.GetBool("WATCHTOWER_HTTP_API_METRICS"), - "Runs Watchtower with the Prometheus metrics API enabled") - - flags.String( - "http-api-token", - "", // viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), - "Sets an authentication token to HTTP API requests.") - flags.BoolP( - "http-api-periodic-polls", - "", - viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"), - "Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled") - // https://no-color.org/ - flags.BoolP( - "no-color", - "", - false, // viper.IsSet("NO_COLOR"), - "Disable ANSI color escape codes in log output") - flags.String( - "scope", - "", // viper.GetString("WATCHTOWER_SCOPE"), - "Defines a monitoring scope for the Watchtower instance.") -} - -// RegisterNotificationFlags that are used by watchtower to send notifications -func RegisterNotificationFlags(rootCmd *cobra.Command) { - flags := rootCmd.PersistentFlags() - - flags.StringSliceP( - "notifications", - "n", - []string{}, // viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), - " Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") - - flags.String( - "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.Int( - "notifications-delay", - 0, // viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"), - "Delay before sending notifications, expressed in seconds") - - flags.String( - "notifications-hostname", - "", - // viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), - "Custom hostname for notification titles") - - flags.String( - "notification-template", - "", - // viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), - "The shoutrrr text/template for the messages") - - flags.StringArray( - "notification-url", - []string{}, - // viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), - "The shoutrrr URL to send notifications to") - - flags.Bool("notification-report", - false, - // viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"), - "Use the session report as the notification template data") - - flags.String( - "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") - - RegisterLegacyNotificationFlags(flags) -} - -func mustBindEnv(flag string, env string) { - if err := viper.BindEnv(flag, env); err != nil { - log.Fatalf("failed to bind env %q to flag %q: %v", env, flag, err) - } -} - -// SetEnvBindings binds environment variables to their corresponding config keys -func SetEnvBindings() { - - // Using WATCHTOWER as a prefix... - viper.SetEnvPrefix("WATCHTOWER") - // ...and replacing dashes with undescores - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - // ...map all environment variables to corresponding flags in upper case - viper.AutomaticEnv() - - // Aliases for non-matching ENV keys (backwards compability) - mustBindEnv("interval", "WATCHTOWER_POLL_INTERVAL") - - // Aliases for DOCKER_-prefixed env variables (matching those used for docker cli) - mustBindEnv("host", "DOCKER_HOST") - mustBindEnv("tlsverify", "DOCKER_TLS_VERIFY") - mustBindEnv("api-version", "DOCKER_API_VERSION") -} - -// 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() error { - var err error - - host := viper.GetString("host") - tls := viper.GetBool("tlsverify") - version := viper.GetString("api-version") - if err = setEnvOptStr("DOCKER_HOST", host); err != nil { - return err - } - if err = setEnvOptBool("DOCKER_TLS_VERIFY", tls); err != nil { - return err - } - if err = setEnvOptStr("DOCKER_API_VERSION", version); err != nil { - return err - } - return nil -} - -// ReadFlags reads common flags used in the main program flow of watchtower -func ReadFlags() (cleanup bool, noRestart bool, monitorOnly bool, timeout time.Duration) { - - cleanup = viper.GetBool("cleanup") - noRestart = viper.GetBool("no-restart") - monitorOnly = viper.GetBool("monitor-only") - timeout = viper.GetDuration("stop-timeout") - - return -} - -func setEnvOptStr(env string, opt string) error { - if opt == "" || opt == os.Getenv(env) { - return nil - } - err := os.Setenv(env, opt) - if err != nil { - return err - } - return nil -} - -func setEnvOptBool(env string, opt bool) error { - if opt { - return setEnvOptStr(env, "1") - } - return nil -} - -// 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() { - secrets := []string{ - "notification-email-server-password", - "notification-slack-hook-url", - "notification-msteams-hook", - "notification-gotify-token", - } - for _, secret := range secrets { - 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(secret string) { - value := viper.GetString(secret) - if value != "" && isFile(value) { - file, err := ioutil.ReadFile(value) - if err != nil { - log.Fatal(err) - } - viper.Set(secret, strings.TrimSpace(string(file))) - } -} - -func isFile(s string) bool { - _, err := os.Stat(s) - if os.IsNotExist(err) { - return false - } - return true -} diff --git a/pkg/container/client.go b/pkg/container/client.go index 726c5f2..dc02bb5 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -3,11 +3,11 @@ package container import ( "bytes" "fmt" + "github.com/containrrr/watchtower/internal/config" "io/ioutil" "strings" "time" - "github.com/containrrr/watchtower/internal/flags" "github.com/containrrr/watchtower/pkg/registry" "github.com/containrrr/watchtower/pkg/registry/digest" t "github.com/containrrr/watchtower/pkg/types" @@ -43,7 +43,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(c *flags.WatchConfig) Client { +func NewClient(c *config.WatchConfig) Client { cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) if err != nil { diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 5de7026..8957103 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -2,12 +2,12 @@ package notifications_test import ( "fmt" + "github.com/containrrr/watchtower/internal/config" "net/url" "os" "time" "github.com/containrrr/watchtower/cmd" - "github.com/containrrr/watchtower/internal/flags" "github.com/containrrr/watchtower/pkg/notifications" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -17,30 +17,20 @@ var _ = Describe("notifications", func() { Describe("the notifier", func() { When("only empty notifier types are provided", func() { - command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) - flags.BindViperFlags(command) - - err := command.ParseFlags([]string{ + parseCommandLine( "--notifications", "shoutrrr", - }) - Expect(err).NotTo(HaveOccurred()) + ) notif := notifications.NewNotifier() - Expect(notif.GetNames()).To(BeEmpty()) }) When("title is overriden in flag", func() { It("should use the specified hostname in the title", func() { - command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) - flags.BindViperFlags(command) - - err := command.ParseFlags([]string{ + parseCommandLine( "--notifications-hostname", "test.host", - }) - Expect(err).NotTo(HaveOccurred()) + ) + hostname := notifications.GetHostname() title := notifications.GetTitle(hostname) Expect(title).To(Equal("Watchtower updates on test.host")) @@ -54,9 +44,7 @@ var _ = Describe("notifications", func() { }) When("no delay is defined", func() { It("should use the default delay", func() { - command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) - flags.BindViperFlags(command) + parseCommandLine() delay := notifications.GetDelay(time.Duration(0)) Expect(delay).To(Equal(time.Duration(0))) @@ -64,39 +52,25 @@ var _ = Describe("notifications", func() { }) When("delay is defined", func() { It("should use the specified delay", func() { - command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) - flags.BindViperFlags(command) - - err := command.ParseFlags([]string{ + parseCommandLine( "--notifications-delay", "5", - }) - Expect(err).NotTo(HaveOccurred()) + ) delay := notifications.GetDelay(time.Duration(0)) Expect(delay).To(Equal(time.Duration(5) * time.Second)) }) }) When("legacy delay is defined", func() { It("should use the specified legacy delay", func() { - command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) - flags.BindViperFlags(command) + parseCommandLine() delay := notifications.GetDelay(time.Duration(5) * time.Second) Expect(delay).To(Equal(time.Duration(5) * time.Second)) }) }) When("legacy delay and delay is defined", func() { It("should use the specified legacy delay and ignore the specified delay", func() { - command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) - flags.BindViperFlags(command) + parseCommandLine("--notifications-delay", "0") - err := command.ParseFlags([]string{ - "--notifications-delay", - "0", - }) - Expect(err).NotTo(HaveOccurred()) delay := notifications.GetDelay(time.Duration(7) * time.Second) Expect(delay).To(Equal(time.Duration(7) * time.Second)) }) @@ -132,7 +106,7 @@ var _ = Describe("notifications", func() { }) When("converting a slack service config into a shoutrrr url", func() { command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) + config.RegisterNotificationOptions(command) username := "containrrrbot" tokenA := "AAAAAAAAA" tokenB := "BBBBBBBBB" @@ -294,6 +268,14 @@ var _ = Describe("notifications", func() { }) }) +func parseCommandLine(args ...string) { + command := cmd.NewRootCommand() + config.RegisterNotificationOptions(command) + config.BindViperFlags(command) + + ExpectWithOffset(1, command.ParseFlags(args)).To(Succeed()) +} + func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string { hostname, err := os.Hostname() Expect(err).NotTo(HaveOccurred()) @@ -313,19 +295,12 @@ func buildExpectedURL(username string, password string, host string, port int, f func testURL(args []string, expectedURL string, expectedDelay time.Duration) { defer GinkgoRecover() - command := cmd.NewRootCommand() - flags.RegisterNotificationFlags(command) - flags.BindViperFlags(command) - - err := command.ParseFlags(args) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + parseCommandLine(args...) hostname := notifications.GetHostname() title := notifications.GetTitle(hostname) urls, delay := notifications.AppendLegacyUrls([]string{}, title) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - ExpectWithOffset(1, urls).To(ContainElement(expectedURL)) ExpectWithOffset(1, delay).To(Equal(expectedDelay)) } diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index dbdd9eb..4cebc90 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -1,11 +1,11 @@ package notifications import ( + "github.com/containrrr/watchtower/internal/config" "time" "github.com/containrrr/shoutrrr/pkg/types" "github.com/containrrr/watchtower/internal/actions/mocks" - "github.com/containrrr/watchtower/internal/flags" s "github.com/containrrr/watchtower/pkg/session" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -75,7 +75,7 @@ var _ = Describe("Shoutrrr", func() { When("no custom template is provided", func() { It("should format the messages using the default template", func() { cmd := new(cobra.Command) - flags.RegisterNotificationFlags(cmd) + config.RegisterNotificationOptions(cmd) shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true)