From 1d5a8d9a4c056600716ae85955db24fdc77cdb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 16 Sep 2023 17:04:44 +0200 Subject: [PATCH] test: check flag/docs consistency (#1770) --- docs/arguments.md | 21 +++++- docs/notifications.md | 5 +- internal/flags/flags.go | 141 +++++++++++++++++++++-------------- internal/flags/flags_test.go | 68 ++++++++++++++++- 4 files changed, 171 insertions(+), 64 deletions(-) diff --git a/docs/arguments.md b/docs/arguments.md index f74b102..95d75d0 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -178,7 +178,7 @@ Environment Variable: WATCHTOWER_INCLUDE_RESTARTING Will also include created and exited containers. ```text - Argument: --include-stopped + Argument: --include-stopped, -S Environment Variable: WATCHTOWER_INCLUDE_STOPPED Type: Boolean Default: false @@ -278,7 +278,7 @@ Environment Variable: WATCHTOWER_NO_STARTUP_MESSAGE Run an update attempt against a container name list one time immediately and exit. ```text - Argument: --run-once + Argument: --run-once, -R Environment Variable: WATCHTOWER_RUN_ONCE Type: Boolean Default: false @@ -392,3 +392,20 @@ Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE Possible values: always, auto, never Default: auto ``` +## Programatic Output (porcelain) + +Writes the session results to STDOUT using a stable, machine-readable format (indicated by the argument VERSION). + +Alias for: + +```text + --notification-url logger:// + --notification-log-stdout + --notification-report + --notification-template porcelain.VERSION.summary-no-log + + Argument: --porcelain, -P +Environment Variable: WATCHTOWER_PORCELAIN + Possible values: v1 + Default: - +``` diff --git a/docs/notifications.md b/docs/notifications.md index efad0a5..3042919 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -20,8 +20,9 @@ system, [logrus](http://github.com/sirupsen/logrus). - `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname. - `--notifications-delay` (env. `WATCHTOWER_NOTIFICATIONS_DELAY`): Delay before sending notifications expressed in seconds. - Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument. -- `notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers. -- `notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together. +- `--notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers. +- `--notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together. +- `--notification-log-stdout` (env. `WATCHTOWER_NOTIFICATION_LOG_STDOUT`): Enable output from `logger://` shoutrrr service to stdout. ## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 1195119..f27c39d 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -23,9 +23,9 @@ var defaultInterval = int((time.Hour * 24).Seconds()) // 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", envString("DOCKER_HOST"), "daemon socket to connect to") + flags.BoolP("tlsverify", "v", envBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote") + flags.StringP("api-version", "a", envString("DOCKER_API_VERSION"), "api version to use by docker client") } // RegisterSystemFlags that are used by watchtower to modify the program flow @@ -34,132 +34,132 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.IntP( "interval", "i", - viper.GetInt("WATCHTOWER_POLL_INTERVAL"), + envInt("WATCHTOWER_POLL_INTERVAL"), "Poll interval (in seconds)") flags.StringP( "schedule", "s", - viper.GetString("WATCHTOWER_SCHEDULE"), + envString("WATCHTOWER_SCHEDULE"), "The cron expression which defines when to update") flags.DurationP( "stop-timeout", "t", - viper.GetDuration("WATCHTOWER_TIMEOUT"), + envDuration("WATCHTOWER_TIMEOUT"), "Timeout before a container is forcefully stopped") flags.BoolP( "no-pull", "", - viper.GetBool("WATCHTOWER_NO_PULL"), + envBool("WATCHTOWER_NO_PULL"), "Do not pull any new images") flags.BoolP( "no-restart", "", - viper.GetBool("WATCHTOWER_NO_RESTART"), + envBool("WATCHTOWER_NO_RESTART"), "Do not restart any containers") flags.BoolP( "no-startup-message", "", - viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), + envBool("WATCHTOWER_NO_STARTUP_MESSAGE"), "Prevents watchtower from sending a startup message") flags.BoolP( "cleanup", "c", - viper.GetBool("WATCHTOWER_CLEANUP"), + envBool("WATCHTOWER_CLEANUP"), "Remove previously used images after updating") flags.BoolP( "remove-volumes", "", - viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), + envBool("WATCHTOWER_REMOVE_VOLUMES"), "Remove attached volumes before updating") flags.BoolP( "label-enable", "e", - viper.GetBool("WATCHTOWER_LABEL_ENABLE"), + envBool("WATCHTOWER_LABEL_ENABLE"), "Watch containers where the com.centurylinklabs.watchtower.enable label is true") flags.BoolP( "debug", "d", - viper.GetBool("WATCHTOWER_DEBUG"), + envBool("WATCHTOWER_DEBUG"), "Enable debug mode with verbose logging") flags.BoolP( "trace", "", - viper.GetBool("WATCHTOWER_TRACE"), + envBool("WATCHTOWER_TRACE"), "Enable trace mode with very verbose logging - caution, exposes credentials") flags.BoolP( "monitor-only", "m", - viper.GetBool("WATCHTOWER_MONITOR_ONLY"), + envBool("WATCHTOWER_MONITOR_ONLY"), "Will only monitor for new images, not update the containers") flags.BoolP( "run-once", "R", - viper.GetBool("WATCHTOWER_RUN_ONCE"), + envBool("WATCHTOWER_RUN_ONCE"), "Run once now and exit") flags.BoolP( "include-restarting", "", - viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"), + envBool("WATCHTOWER_INCLUDE_RESTARTING"), "Will also include restarting containers") flags.BoolP( "include-stopped", "S", - viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), + envBool("WATCHTOWER_INCLUDE_STOPPED"), "Will also include created and exited containers") flags.BoolP( "revive-stopped", "", - viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), + envBool("WATCHTOWER_REVIVE_STOPPED"), "Will also start stopped containers that were updated, if include-stopped is active") flags.BoolP( "enable-lifecycle-hooks", "", - viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), + envBool("WATCHTOWER_LIFECYCLE_HOOKS"), "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") flags.BoolP( "rolling-restart", "", - viper.GetBool("WATCHTOWER_ROLLING_RESTART"), + envBool("WATCHTOWER_ROLLING_RESTART"), "Restart containers one at a time") flags.BoolP( "http-api-update", "", - viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"), + envBool("WATCHTOWER_HTTP_API_UPDATE"), "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") flags.BoolP( "http-api-metrics", "", - viper.GetBool("WATCHTOWER_HTTP_API_METRICS"), + envBool("WATCHTOWER_HTTP_API_METRICS"), "Runs Watchtower with the Prometheus metrics API enabled") flags.StringP( "http-api-token", "", - viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), + envString("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"), + envBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"), "Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled") // https://no-color.org/ @@ -172,18 +172,18 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.StringP( "scope", "", - viper.GetString("WATCHTOWER_SCOPE"), + envString("WATCHTOWER_SCOPE"), "Defines a monitoring scope for the Watchtower instance.") flags.StringP( "porcelain", "P", - viper.GetString("WATCHTOWER_PORCELAIN"), + envString("WATCHTOWER_PORCELAIN"), `Write session results to stdout using a stable versioned format. Supported values: "v1"`) flags.String( "log-level", - viper.GetString("WATCHTOWER_LOG_LEVEL"), + envString("WATCHTOWER_LOG_LEVEL"), "The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace") } @@ -194,177 +194,202 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { flags.StringSliceP( "notifications", "n", - viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), + envStringSlice("WATCHTOWER_NOTIFICATIONS"), " Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.String( "notifications-level", - viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), + envString("WATCHTOWER_NOTIFICATIONS_LEVEL"), "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") flags.IntP( "notifications-delay", "", - viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"), + envInt("WATCHTOWER_NOTIFICATIONS_DELAY"), "Delay before sending notifications, expressed in seconds") flags.StringP( "notifications-hostname", "", - viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), + envString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), "Custom hostname for notification titles") flags.StringP( "notification-email-from", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), "Address to send notification emails from") flags.StringP( "notification-email-to", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), "Address to send notification emails to") flags.IntP( "notification-email-delay", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), + envInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), "Delay before sending notifications, expressed in seconds") flags.StringP( "notification-email-server", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), "SMTP server to send notification emails through") flags.IntP( "notification-email-server-port", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), + envInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), "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"), + envBool("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"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), "SMTP server user for sending notifications") flags.StringP( "notification-email-server-password", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), "SMTP server password for sending notifications") flags.StringP( "notification-email-subjecttag", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), "Subject prefix tag for notifications via mail") flags.StringP( "notification-slack-hook-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), + envString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), "The Slack Hook URL to send notifications to") flags.StringP( "notification-slack-identifier", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), + envString("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"), + envString("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"), + envString("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"), + envString("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"), + envString("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"), + envBool("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"), + envString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), "The Gotify URL to send notifications to") flags.StringP( "notification-gotify-token", "", - viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), + envString("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"), + envBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), `Controls whether watchtower verifies the Gotify server's certificate chain and host name. Should only be used for testing.`) flags.String( "notification-template", - viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), + envString("WATCHTOWER_NOTIFICATION_TEMPLATE"), "The shoutrrr text/template for the messages") flags.StringArray( "notification-url", - viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), + envStringSlice("WATCHTOWER_NOTIFICATION_URL"), "The shoutrrr URL to send notifications to") flags.Bool("notification-report", - viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"), + envBool("WATCHTOWER_NOTIFICATION_REPORT"), "Use the session report as the notification template data") flags.StringP( "notification-title-tag", "", - viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"), + envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"), "Title prefix tag for notifications") flags.Bool("notification-skip-title", - viper.GetBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"), + envBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"), "Do not pass the title param to notifications") flags.String( "warn-on-head-failure", - viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), + envString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), "When to warn about HEAD pull requests failing. Possible values: always, auto or never") flags.Bool( "notification-log-stdout", - viper.GetBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"), + envBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"), "Write notification logs to stdout instead of logging (to stderr)") } +func envString(key string) string { + viper.MustBindEnv(key) + return viper.GetString(key) +} + +func envStringSlice(key string) []string { + viper.MustBindEnv(key) + return viper.GetStringSlice(key) +} + +func envInt(key string) int { + viper.MustBindEnv(key) + return viper.GetInt(key) +} + +func envBool(key string) bool { + viper.MustBindEnv(key) + return viper.GetBool(key) +} + +func envDuration(key string) time.Duration { + viper.MustBindEnv(key) + return viper.GetDuration(key) +} + // SetDefaults provides default values for environment variables func SetDefaults() { viper.AutomaticEnv() diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 9d11af6..394b2c7 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,12 +1,16 @@ package flags import ( + "os" + "strings" + "testing" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "os" - "testing" ) func TestEnvConfig_Defaults(t *testing.T) { @@ -222,3 +226,63 @@ func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) { ProcessFlagAliases(flags) }) } + +func TestFlagsArePrecentInDocumentation(t *testing.T) { + + // Legacy notifcations are ignored, since they are (soft) deprecated + ignoredEnvs := map[string]string{ + "WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI": "legacy", + "WATCHTOWER_NOTIFICATION_SLACK_ICON_URL": "legacy", + } + + ignoredFlags := map[string]string{ + "notification-gotify-url": "legacy", + "notification-slack-icon-emoji": "legacy", + "notification-slack-icon-url": "legacy", + } + + cmd := new(cobra.Command) + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + RegisterNotificationFlags(cmd) + + flags := cmd.PersistentFlags() + + docFiles := []string{ + "../../docs/arguments.md", + "../../docs/lifecycle-hooks.md", + "../../docs/notifications.md", + } + allDocs := "" + for _, f := range docFiles { + bytes, err := os.ReadFile(f) + if err != nil { + t.Fatalf("Could not load docs file %q: %v", f, err) + } + allDocs += string(bytes) + } + + flags.VisitAll(func(f *pflag.Flag) { + if !strings.Contains(allDocs, "--"+f.Name) { + if _, found := ignoredFlags[f.Name]; !found { + t.Logf("Docs does not mention flag long name %q", f.Name) + t.Fail() + } + } + if !strings.Contains(allDocs, "-"+f.Shorthand) { + t.Logf("Docs does not mention flag shorthand %q (%q)", f.Shorthand, f.Name) + t.Fail() + } + }) + + for _, key := range viper.AllKeys() { + envKey := strings.ToUpper(key) + if !strings.Contains(allDocs, envKey) { + if _, found := ignoredEnvs[envKey]; !found { + t.Logf("Docs does not mention environment variable %q", envKey) + t.Fail() + } + } + } +}