Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding parameter to allow containers to be updated only after some number of days have passed since the new image has been published #1884

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
disableContainers []string
notifier t.Notifier
timeout time.Duration
delayDays int
lifecycleHooks bool
rollingRestart bool
scope string
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -288,6 +290,10 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
until := formatDuration(time.Until(sched))
startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST"))
startupLog.Info("Note that the first check will be performed in " + until)
delayDays, _ = c.PersistentFlags().GetInt("delay-days")
if delayDays > 0 {
startupLog.Infof("Container updates will be delayed until %d day(s) after image creation.", delayDays)
}
} else if runOnce, _ := c.PersistentFlags().GetBool("run-once"); runOnce {
startupLog.Info("Running a one time update.")
} else {
Expand Down Expand Up @@ -364,6 +370,7 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
NoRestart: noRestart,
Timeout: timeout,
MonitorOnly: monitorOnly,
DelayDays: delayDays,
LifecycleHooks: lifecycleHooks,
RollingRestart: rollingRestart,
LabelPrecedence: labelPrecedence,
Expand Down
10 changes: 10 additions & 0 deletions docs/arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]?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 * * *"`
Expand Down
6 changes: 6 additions & 0 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"",
Expand Down
4 changes: 2 additions & 2 deletions internal/flags/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,9 @@ func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
})
}

func TestFlagsArePrecentInDocumentation(t *testing.T) {
func TestFlagsArePresentInDocumentation(t *testing.T) {

// Legacy notifcations are ignored, since they are (soft) deprecated
// Legacy notifications are ignored, since they are (soft) deprecated
ignoredEnvs := map[string]string{
piksel marked this conversation as resolved.
Show resolved Hide resolved
"WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI": "legacy",
"WATCHTOWER_NOTIFICATION_SLACK_ICON_URL": "legacy",
Expand Down
39 changes: 37 additions & 2 deletions pkg/container/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -343,6 +358,26 @@ func (client dockerClient) HasNewImage(ctx context.Context, container t.Containe
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 was created %d day(s) ago but update delayed until %d day(s) after creation", container.Name(), diffDays, requiredDays)
return false, currentImageID, nil
}
}
}
pjdubya marked this conversation as resolved.
Show resolved Hide resolved

log.Infof("Found new %s image (%s)", imageName, newImageID.ShortID())
return true, newImageID, nil
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/types/update_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down