From a8279b47fef0b1f1e53aaaeef93d619cca781015 Mon Sep 17 00:00:00 2001 From: Peter Wilhelm Date: Sun, 17 Dec 2023 19:58:33 -0600 Subject: [PATCH] Adding new runtime param delay-days that is used to control whether container is updated to new image immediately or wait until the indicated days have passed --- cmd/root.go | 3 +++ docs/arguments.md | 10 ++++++++++ internal/flags/flags.go | 6 ++++++ pkg/container/client.go | 41 +++++++++++++++++++++++++++++++++++--- pkg/types/update_params.go | 3 ++- 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index eef13ce..902b1be 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,6 +39,7 @@ var ( disableContainers []string notifier t.Notifier timeout time.Duration + delayDays int lifecycleHooks bool rollingRestart bool scope string @@ -96,6 +97,7 @@ func PreRun(cmd *cobra.Command, _ []string) { enableLabel, _ = f.GetBool("label-enable") disableContainers, _ = f.GetStringSlice("disable-containers") + delayDays, _ = f.GetInt("delay-days") lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") rollingRestart, _ = f.GetBool("rolling-restart") scope, _ = f.GetString("scope") @@ -364,6 +366,7 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { NoRestart: noRestart, Timeout: timeout, MonitorOnly: monitorOnly, + DelayDays: delayDays, LifecycleHooks: lifecycleHooks, RollingRestart: rollingRestart, LabelPrecedence: labelPrecedence, diff --git a/docs/arguments.md b/docs/arguments.md index d7ed0b0..5f1fd66 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -381,6 +381,16 @@ Environment Variable: WATCHTOWER_HTTP_API_METRICS Default: false ``` +## Delayed Update +Only update container to latest version of image if some number of days have passed since it has been published. This option may be useful for those who wish to avoid updating prior to the new version having some time in the field prior to updating in case there are critical defects found and released in a subsequent version. + +```text + Argument: --delay-days +Environment Variable: WATCHTOWER_DELAY_DAYS + Type: Integer + Default: false +``` + ## Scheduling [Cron expression](https://pkg.go.dev/github.com/robfig/cron@v1.2.0?tab=doc#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression can be defined, but not both. An example: `--schedule "0 0 4 * * *"` diff --git a/internal/flags/flags.go b/internal/flags/flags.go index c11cdae..d5df53f 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -147,6 +147,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { envBool("WATCHTOWER_LIFECYCLE_HOOKS"), "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") + flags.IntP( + "delay-days", + "0", + envInt("WATCHTOWER_DELAY_DAYS"), + "Number of days to wait for new image version to be in place prior to installing it") + flags.BoolP( "rolling-restart", "", diff --git a/pkg/container/client.go b/pkg/container/client.go index c6c37de..7533a9f 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -325,10 +325,25 @@ func (client dockerClient) IsContainerStale(container t.Container, params t.Upda return false, container.SafeImageID(), err } - return client.HasNewImage(ctx, container) + return client.HasNewImage(ctx, container, params) } -func (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) { +// Date strings sometimes vary in how many digits after the decimal point are present. This function +// standardizes them by removing the milliseconds. +func truncateMilliseconds(dateString string) string { + // Find the position of the dot (.) in the date string + dotIndex := strings.Index(dateString, ".") + + // If the dot is found, truncate the string before the dot + if dotIndex != -1 { + return dateString[:dotIndex] + "Z" + } + + // If the dot is not found, return the original string + return dateString +} + +func (client dockerClient) HasNewImage(ctx context.Context, container t.Container, params t.UpdateParams) (hasNew bool, latestImage t.ImageID, err error) { currentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image) imageName := container.ImageName() @@ -339,10 +354,30 @@ func (client dockerClient) HasNewImage(ctx context.Context, container t.Containe newImageID := t.ImageID(newImageInfo.ID) if newImageID == currentImageID { - log.Debugf("No new images found for %s", container.Name()) + log.Debugf("No new images found for %s [ imageID %s ]", container.Name(), newImageID.ShortID()) return false, currentImageID, nil } + // Disabled by default + if params.DelayDays > 0 { + // Define the layout string for the date format without milliseconds + layout := "2006-01-02T15:04:05Z" + newImageDate, error := time.Parse(layout, truncateMilliseconds(newImageInfo.Created)) + + if error != nil { + log.Errorf("Error parsing Created date (%s) for container %s latest label. Error: %s", newImageInfo.Created, container.Name(), error) + return false, currentImageID, nil + } else { + requiredDays := params.DelayDays + diffDays := int(time.Since(newImageDate).Hours() / 24) + + if diffDays < requiredDays { + log.Infof("New image found for %s that is %d days since publication but update delayed until %d days", container.Name(), diffDays, requiredDays) + return false, currentImageID, nil + } + } + } + log.Infof("Found new %s image (%s)", imageName, newImageID.ShortID()) return true, newImageID, nil } diff --git a/pkg/types/update_params.go b/pkg/types/update_params.go index 2b6d3c4..eee1b7e 100644 --- a/pkg/types/update_params.go +++ b/pkg/types/update_params.go @@ -11,7 +11,8 @@ type UpdateParams struct { NoRestart bool Timeout time.Duration MonitorOnly bool - NoPull bool + NoPull bool + DelayDays int LifecycleHooks bool RollingRestart bool LabelPrecedence bool