mirror of
https://github.com/containrrr/watchtower.git
synced 2026-02-22 07:04:07 +01:00
Merge branch 'main' into healthcheck
This commit is contained in:
commit
246da4da68
31 changed files with 745 additions and 986 deletions
|
|
@ -86,7 +86,7 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int)
|
|||
}
|
||||
|
||||
// IsContainerStale is true if not explicitly stated in TestData for the mock client
|
||||
func (client MockClient) IsContainerStale(cont t.Container) (bool, t.ImageID, error) {
|
||||
func (client MockClient) IsContainerStale(cont t.Container, params t.UpdateParams) (bool, t.ImageID, error) {
|
||||
stale, found := client.TestData.Staleness[cont.Name()]
|
||||
if !found {
|
||||
stale = true
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
staleCheckFailed := 0
|
||||
|
||||
for i, targetContainer := range containers {
|
||||
stale, newestImage, err := client.IsContainerStale(targetContainer)
|
||||
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
|
||||
stale, newestImage, err := client.IsContainerStale(targetContainer, params)
|
||||
shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params)
|
||||
if err == nil && shouldUpdate {
|
||||
// Check to make sure we have all the necessary information for recreating the container
|
||||
err = targetContainer.VerifyConfiguration()
|
||||
|
|
@ -72,12 +72,10 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
UpdateImplicitRestart(containers)
|
||||
|
||||
var containersToUpdate []types.Container
|
||||
if !params.MonitorOnly {
|
||||
for _, c := range containers {
|
||||
if !c.IsMonitorOnly() {
|
||||
containersToUpdate = append(containersToUpdate, c)
|
||||
progress.MarkForUpdate(c.ID())
|
||||
}
|
||||
for _, c := range containers {
|
||||
if !c.IsMonitorOnly(params) {
|
||||
containersToUpdate = append(containersToUpdate, c)
|
||||
progress.MarkForUpdate(c.ID())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,12 +178,84 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{MonitorOnly: true})
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
})
|
||||
When("watchtower has been instructed to have label take precedence", func() {
|
||||
It("it should update containers when monitor only is set to false", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image2:latest",
|
||||
false,
|
||||
false,
|
||||
time.Now(),
|
||||
&dockerContainer.Config{
|
||||
Labels: map[string]string{
|
||||
"com.centurylinklabs.watchtower.monitor-only": "false",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
})
|
||||
It("it should update not containers when monitor only is set to true", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image2:latest",
|
||||
false,
|
||||
false,
|
||||
time.Now(),
|
||||
&dockerContainer.Config{
|
||||
Labels: map[string]string{
|
||||
"com.centurylinklabs.watchtower.monitor-only": "true",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
It("it should update not containers when monitor only is not set", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
"fake-image:latest",
|
||||
time.Now()),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("watchtower has been instructed to run lifecycle hooks", func() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -24,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
|
||||
|
|
@ -35,132 +34,138 @@ 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.StringP(
|
||||
"log-format",
|
||||
"l",
|
||||
viper.GetString("WATCHTOWER_LOG_FORMAT"),
|
||||
"Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON")
|
||||
|
||||
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/
|
||||
|
|
@ -173,18 +178,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")
|
||||
|
||||
flags.BoolP(
|
||||
|
|
@ -192,6 +197,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
|||
"",
|
||||
false,
|
||||
"Do health check and exit")
|
||||
|
||||
flags.BoolP(
|
||||
"label-take-precedence",
|
||||
"",
|
||||
envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"),
|
||||
"Label applied to containers take precedence over arguments")
|
||||
}
|
||||
|
||||
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||
|
|
@ -201,177 +212,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()
|
||||
|
|
@ -385,6 +421,7 @@ func SetDefaults() {
|
|||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
|
||||
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
|
||||
viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto")
|
||||
}
|
||||
|
||||
// EnvConfig translates the command-line options into environment variables
|
||||
|
|
@ -473,14 +510,17 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) {
|
|||
"notification-msteams-hook",
|
||||
"notification-gotify-token",
|
||||
"notification-url",
|
||||
"http-api-token",
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
getSecretFromFile(flags, secret)
|
||||
if err := getSecretFromFile(flags, secret); err != nil {
|
||||
log.Fatalf("failed to get secret from flag %v: %s", secret, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file.
|
||||
func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
||||
func getSecretFromFile(flags *pflag.FlagSet, secret string) error {
|
||||
flag := flags.Lookup(secret)
|
||||
if sliceValue, ok := flag.Value.(pflag.SliceValue); ok {
|
||||
oldValues := sliceValue.GetSlice()
|
||||
|
|
@ -489,7 +529,7 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
|||
if value != "" && isFile(value) {
|
||||
file, err := os.Open(value)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
|
|
@ -499,25 +539,26 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
|||
}
|
||||
values = append(values, line)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
sliceValue.Replace(values)
|
||||
return
|
||||
return sliceValue.Replace(values)
|
||||
}
|
||||
|
||||
value := flag.Value.String()
|
||||
if value != "" && isFile(value) {
|
||||
file, err := ioutil.ReadFile(value)
|
||||
content, err := os.ReadFile(value)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = flags.Set(secret, strings.TrimSpace(string(file)))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
return flags.Set(secret, strings.TrimSpace(string(content)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isFile(s string) bool {
|
||||
|
|
@ -570,19 +611,59 @@ func ProcessFlagAliases(flags *pflag.FlagSet) {
|
|||
// update schedule flag to match interval if it's set, or to the default if none of them are
|
||||
if intervalChanged || !scheduleChanged {
|
||||
interval, _ := flags.GetInt(`interval`)
|
||||
flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||
_ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `debug`) {
|
||||
flags.Set(`log-level`, `debug`)
|
||||
_ = flags.Set(`log-level`, `debug`)
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `trace`) {
|
||||
flags.Set(`log-level`, `trace`)
|
||||
_ = flags.Set(`log-level`, `trace`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger
|
||||
func SetupLogging(f *pflag.FlagSet) error {
|
||||
logFormat, _ := f.GetString(`log-format`)
|
||||
noColor, _ := f.GetBool("no-color")
|
||||
|
||||
switch strings.ToLower(logFormat) {
|
||||
case "auto":
|
||||
// This will either use the "pretty" or "logfmt" format, based on whether the standard out is connected to a TTY
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: noColor,
|
||||
// enable logrus built-in support for https://bixense.com/clicolors/
|
||||
EnvironmentOverrideColors: true,
|
||||
})
|
||||
case "json":
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
case "logfmt":
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: true,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
case "pretty":
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
// "Pretty" format combined with `--no-color` will only change the timestamp to the time since start
|
||||
ForceColors: !noColor,
|
||||
FullTimestamp: false,
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("invalid log format: %s", logFormat)
|
||||
}
|
||||
|
||||
rawLogLevel, _ := f.GetString(`log-level`)
|
||||
if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
|
||||
return fmt.Errorf("invalid log level: %e", err)
|
||||
} else {
|
||||
log.SetLevel(logLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func flagIsEnabled(flags *pflag.FlagSet, name string) bool {
|
||||
value, err := flags.GetBool(name)
|
||||
if err != nil {
|
||||
|
|
@ -599,7 +680,7 @@ func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error
|
|||
|
||||
if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
|
||||
for _, value := range values {
|
||||
flagValues.Append(value)
|
||||
_ = flagValues.Append(value)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
package flags
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestEnvConfig_Defaults(t *testing.T) {
|
||||
// Unset testing environments own variables, since those are not what is under test
|
||||
os.Unsetenv("DOCKER_TLS_VERIFY")
|
||||
os.Unsetenv("DOCKER_HOST")
|
||||
_ = os.Unsetenv("DOCKER_TLS_VERIFY")
|
||||
_ = os.Unsetenv("DOCKER_HOST")
|
||||
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
|
|
@ -48,10 +50,7 @@ func TestEnvConfig_Custom(t *testing.T) {
|
|||
|
||||
func TestGetSecretsFromFilesWithString(t *testing.T) {
|
||||
value := "supersecretstring"
|
||||
|
||||
err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
|
||||
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||
}
|
||||
|
|
@ -60,18 +59,15 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) {
|
|||
value := "megasecretstring"
|
||||
|
||||
// Create the temporary file which will contain a secret.
|
||||
file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
|
||||
file, err := os.CreateTemp(t.TempDir(), "watchtower-")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
|
||||
|
||||
// Write the secret to the temporary file.
|
||||
secret := []byte(value)
|
||||
_, err = file.Write(secret)
|
||||
_, err = file.Write([]byte(value))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
|
||||
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||
}
|
||||
|
|
@ -80,16 +76,15 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
|
|||
values := []string{"entry2", "", "entry3"}
|
||||
|
||||
// Create the temporary file which will contain a secret.
|
||||
file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
|
||||
file, err := os.CreateTemp(t.TempDir(), "watchtower-")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
|
||||
|
||||
// Write the secret to the temporary file.
|
||||
for _, value := range values {
|
||||
_, err = file.WriteString("\n" + value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
file.Close()
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`,
|
||||
`--notification-url`, "entry1",
|
||||
|
|
@ -99,6 +94,7 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
|
|||
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
require.NoError(t, cmd.ParseFlags(args))
|
||||
GetSecretsFromFiles(cmd)
|
||||
|
|
@ -166,9 +162,7 @@ func TestProcessFlagAliases(t *testing.T) {
|
|||
|
||||
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
err := os.Setenv("WATCHTOWER_DEBUG", `true`)
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_DEBUG")
|
||||
t.Setenv("WATCHTOWER_DEBUG", `true`)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
|
|
@ -183,6 +177,57 @@ func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
|||
assert.Equal(t, `debug`, logLevel)
|
||||
}
|
||||
|
||||
func TestLogFormatFlag(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
|
||||
// Ensure the default value is Auto
|
||||
require.NoError(t, cmd.ParseFlags([]string{}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
|
||||
|
||||
// Test JSON format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter)
|
||||
|
||||
// Test Pretty format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
|
||||
textFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, textFormatter.ForceColors)
|
||||
assert.False(t, textFormatter.FullTimestamp)
|
||||
|
||||
// Test LogFmt format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
textFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, textFormatter.DisableColors)
|
||||
assert.True(t, textFormatter.FullTimestamp)
|
||||
|
||||
// Test invalid format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`}))
|
||||
require.Error(t, SetupLogging(cmd.Flags()))
|
||||
}
|
||||
|
||||
func TestLogLevelFlag(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
|
||||
// Test invalid format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`}))
|
||||
require.Error(t, SetupLogging(cmd.Flags()))
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
||||
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
||||
cmd := new(cobra.Command)
|
||||
|
|
@ -202,9 +247,7 @@ func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
|||
func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
err := os.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_SCHEDULE")
|
||||
t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
|
|
@ -234,3 +277,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue