From 0d1d7979cb91f261abb44dbfd1cb54ce97e3d804 Mon Sep 17 00:00:00 2001 From: Peter Wilhelm Date: Sun, 7 Jan 2024 17:56:17 -0600 Subject: [PATCH 1/6] Add test cases and data for testing defer-days functionality --- internal/actions/mocks/container.go | 7 ++++ internal/actions/update_test.go | 60 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index e830587..86f99b5 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -79,6 +79,13 @@ func CreateMockContainerWithDigest(id string, name string, image string, created return c } +// CreateMockContainerWithDigest should only be used for testing +func CreateMockContainerWithImageCreatedTime(id string, name string, image string, created time.Time, imageCreated time.Time) wt.Container { + c := CreateMockContainer(id, name, image, created) + c.ImageInfo().Created = imageCreated.UTC().Format(time.RFC3339) + return c +} + // CreateMockContainerWithConfig creates a container substitute valid for testing func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) wt.Container { content := types.ContainerJSON{ diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go index 9209dcd..e3a5df9 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -65,6 +65,35 @@ func getLinkedTestData(withImageInfo bool) *TestData { } } +func getMixedAgeTestData(keepContainer string) *TestData { + return &TestData{ + NameOfContainerToKeep: keepContainer, + Containers: []types.Container{ + // new container with 5 day old image + CreateMockContainerWithImageCreatedTime( + "test-container-01", + "test-container-01", + "fake-image-01:latest", + time.Now(), + time.Now().AddDate(0, 0, -5)), + // new container with 1 day old image + CreateMockContainerWithImageCreatedTime( + "test-container-02", + "test-container-02", + "fake-image-02:latest", + time.Now(), + time.Now().AddDate(0, 0, -1)), + // new container with 1 hour old image + CreateMockContainerWithImageCreatedTime( + "test-container-03", + "test-container-03", + "fake-image-03:latest", + time.Now(), + time.Now().Add(-1*time.Hour)), + }, + } +} + var _ = Describe("the update action", func() { When("watchtower has been instructed to clean up", func() { When("there are multiple containers using the same image", func() { @@ -258,6 +287,37 @@ var _ = Describe("the update action", func() { }) }) + When("watchtower has been instructed to defer updates by some number of days", func() { + It("should only update the 1 container with image at least 2 days old when DeferDays is 2", func() { + client := CreateMockClient(getMixedAgeTestData(""), false, false) + report, err := actions.Update(client, types.UpdateParams{DeferDays: 2}) + Expect(err).NotTo(HaveOccurred()) + Expect(report.Updated()).To(HaveLen(1)) + Expect(report.Deferred()).To(HaveLen(2)) + }) + It("should only update the 2 containers with image at least 1 day old when DeferDays is 1", func() { + client := CreateMockClient(getMixedAgeTestData(""), false, false) + report, err := actions.Update(client, types.UpdateParams{DeferDays: 1}) + Expect(err).NotTo(HaveOccurred()) + Expect(report.Updated()).To(HaveLen(2)) + Expect(report.Deferred()).To(HaveLen(1)) + }) + It("should update all containers when DeferDays is 0", func() { + client := CreateMockClient(getMixedAgeTestData(""), false, false) + report, err := actions.Update(client, types.UpdateParams{DeferDays: 0}) + Expect(err).NotTo(HaveOccurred()) + Expect(report.Updated()).To(HaveLen(3)) + Expect(report.Deferred()).To(HaveLen(0)) + }) + It("should update all containers when DeferDays is not specified", func() { + client := CreateMockClient(getMixedAgeTestData(""), false, false) + report, err := actions.Update(client, types.UpdateParams{}) + Expect(err).NotTo(HaveOccurred()) + Expect(report.Updated()).To(HaveLen(3)) + Expect(report.Deferred()).To(HaveLen(0)) + }) + }) + When("watchtower has been instructed to run lifecycle hooks", func() { When("pre-update script returns 1", func() { From 223358f7a0e552551f8e1a8b98c7283ae3077bdb Mon Sep 17 00:00:00 2001 From: Peter Wilhelm Date: Sun, 7 Jan 2024 17:59:10 -0600 Subject: [PATCH 2/6] Change parameter name from delay-days to defer-days to be clearer to user and align with status reporting --- cmd/root.go | 19 ++++++++++--------- docs/arguments.md | 6 +++--- internal/flags/flags.go | 4 ++-- pkg/types/update_params.go | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 65c32f3..47e43d5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,7 +39,7 @@ var ( disableContainers []string notifier t.Notifier timeout time.Duration - delayDays int + deferDays int lifecycleHooks bool rollingRestart bool scope string @@ -97,7 +97,7 @@ func PreRun(cmd *cobra.Command, _ []string) { enableLabel, _ = f.GetBool("label-enable") disableContainers, _ = f.GetStringSlice("disable-containers") - delayDays, _ = f.GetInt("delay-days") + deferDays, _ = f.GetInt("defer-days") lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") rollingRestart, _ = f.GetBool("rolling-restart") scope, _ = f.GetString("scope") @@ -290,9 +290,9 @@ 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) + deferDays, _ = c.PersistentFlags().GetInt("defer-days") + if deferDays > 0 { + startupLog.Infof("Container updates will be deferred until %d day(s) after image creation.", deferDays) } } else if runOnce, _ := c.PersistentFlags().GetBool("run-once"); runOnce { startupLog.Info("Running a one time update.") @@ -370,7 +370,7 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { NoRestart: noRestart, Timeout: timeout, MonitorOnly: monitorOnly, - DelayDays: delayDays, + DeferDays: deferDays, LifecycleHooks: lifecycleHooks, RollingRestart: rollingRestart, LabelPrecedence: labelPrecedence, @@ -383,9 +383,10 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { notifier.SendNotification(result) metricResults := metrics.NewMetric(result) notifications.LocalLog.WithFields(log.Fields{ - "Scanned": metricResults.Scanned, - "Updated": metricResults.Updated, - "Failed": metricResults.Failed, + "Scanned": metricResults.Scanned, + "Updated": metricResults.Updated, + "Deferred": metricResults.Deferred, + "Failed": metricResults.Failed, }).Info("Session done") return metricResults } diff --git a/docs/arguments.md b/docs/arguments.md index 5f1fd66..4b75d42 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -381,12 +381,12 @@ Environment Variable: WATCHTOWER_HTTP_API_METRICS Default: false ``` -## Delayed Update +## Deferred 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 + Argument: --defer-days +Environment Variable: WATCHTOWER_DEFER_DAYS Type: Integer Default: false ``` diff --git a/internal/flags/flags.go b/internal/flags/flags.go index d5df53f..de7c4f8 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -148,9 +148,9 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") flags.IntP( - "delay-days", + "defer-days", "0", - envInt("WATCHTOWER_DELAY_DAYS"), + envInt("WATCHTOWER_DEFER_DAYS"), "Number of days to wait for new image version to be in place prior to installing it") flags.BoolP( diff --git a/pkg/types/update_params.go b/pkg/types/update_params.go index eee1b7e..a6563fa 100644 --- a/pkg/types/update_params.go +++ b/pkg/types/update_params.go @@ -12,7 +12,7 @@ type UpdateParams struct { Timeout time.Duration MonitorOnly bool NoPull bool - DelayDays int + DeferDays int LifecycleHooks bool RollingRestart bool LabelPrecedence bool From 48bfaef350276b9cc38ff262d92f75df9745c108 Mon Sep 17 00:00:00 2001 From: Peter Wilhelm Date: Sun, 7 Jan 2024 18:01:59 -0600 Subject: [PATCH 3/6] Simplify logic for DeferDays (renamed from DelayDays) to improve readability; capture and report err in case of getImageAgeDays problem; simplify getImageAgeDays logic; use new MarkDeferred in progress report for deferred items --- internal/actions/update.go | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/internal/actions/update.go b/internal/actions/update.go index 63196ce..642010f 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -2,7 +2,6 @@ package actions import ( "errors" - "strings" "time" "github.com/containrrr/watchtower/internal/util" @@ -38,16 +37,17 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e // stale will be true if there is a more recent image than the current container is using stale, newestImage, err := client.IsContainerStale(targetContainer, params) shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params) - imageUpdateDelayResolved := true + imageUpdateDelayed := false imageAgeDays := 0 if err == nil && shouldUpdate { // Check to make sure we have all the necessary information for recreating the container, including ImageInfo err = targetContainer.VerifyConfiguration() if err == nil { - if params.DelayDays > 0 { - imageAgeDays, err := getImageAgeDays(targetContainer.ImageInfo().Created) + if params.DeferDays > 0 { + imageAgeDays, imageErr := getImageAgeDays(targetContainer.ImageInfo().Created) + err = imageErr if err == nil { - imageUpdateDelayResolved = imageAgeDays >= params.DelayDays + imageUpdateDelayed = imageAgeDays < params.DeferDays } } } else if log.IsLevelEnabled(log.TraceLevel) { @@ -66,11 +66,12 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e stale = false staleCheckFailed++ progress.AddSkipped(targetContainer, err) - } else if !imageUpdateDelayResolved { - log.Infof("New image found for %s that was created %d day(s) ago but update delayed until %d day(s) after creation", targetContainer.Name(), imageAgeDays, params.DelayDays) + } else if imageUpdateDelayed { + log.Infof("New image found for %s that was created %d day(s) ago but update deferred until %d day(s) after creation", targetContainer.Name(), imageAgeDays, params.DeferDays) // technically the container is stale but we set it to false here because it is this stale flag that tells downstream methods whether to perform the update stale = false progress.AddScanned(targetContainer, newestImage) + progress.MarkDeferred(targetContainer.ID()) } else { progress.AddScanned(targetContainer, newestImage) } @@ -90,9 +91,11 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e // containersToUpdate will contain all containers, not just those that need to be updated. The "stale" flag is checked via container.ToRestart() // within stopContainersInReversedOrder and restartContainersInSortedOrder to skip containers with stale set to false (unless LinkedToRestarting set) + // NOTE: This logic is changing with latest PR on main repo var containersToUpdate []types.Container for _, c := range containers { - if !c.IsMonitorOnly(params) { + // pulling this change in from PR 1895 for now to avoid updating status incorrectly + if c.ToRestart() && !c.IsMonitorOnly(params) { containersToUpdate = append(containersToUpdate, c) progress.MarkForUpdate(c.ID()) } @@ -286,19 +289,10 @@ func linkedContainerMarkedForRestart(links []string, containers []types.Containe } // Finds the difference between now and a given date, in full days. Input date is expected to originate -// from an image's Created attribute in ISO 8601, but since these do not always contain the same number of -// digits for milliseconds, the function also accounts for variations. +// from an image's Created attribute which will follow ISO 3339/8601 format. +// Reference: https://docs.docker.com/engine/api/v1.43/#tag/Image/operation/ImageInspect func getImageAgeDays(imageCreatedDateTime string) (int, error) { - - // Date strings sometimes vary in how many digits after the decimal point are present. If present, drop millisecond portion to standardize. - dotIndex := strings.Index(imageCreatedDateTime, ".") - if dotIndex != -1 { - imageCreatedDateTime = imageCreatedDateTime[:dotIndex] + "Z" - } - - // Define the layout string for the date format without milliseconds - layout := "2006-01-02T15:04:05Z" - imageCreatedDate, error := time.Parse(layout, imageCreatedDateTime) + imageCreatedDate, error := time.Parse(time.RFC3339Nano, imageCreatedDateTime) if error != nil { log.Errorf("Error parsing imageCreatedDateTime date (%s). Error: %s", imageCreatedDateTime, error) From 3c0441b94c9188419f34a6791f0a6f7b6ef276d3 Mon Sep 17 00:00:00 2001 From: Peter Wilhelm Date: Sun, 7 Jan 2024 18:04:47 -0600 Subject: [PATCH 4/6] Add metrics and progress report capabilities for deferred items seperate from other statuses --- pkg/metrics/metrics.go | 31 +++++++++++++-------- pkg/notifications/json.go | 13 +++++---- pkg/notifications/json_test.go | 3 +- pkg/notifications/preview/data/data.go | 2 ++ pkg/notifications/preview/data/report.go | 32 ++++++++++++++-------- pkg/session/container_status.go | 3 ++ pkg/session/progress.go | 5 ++++ pkg/session/report.go | 35 ++++++++++++++++-------- pkg/types/report.go | 1 + tplprev/main.go | 2 +- 10 files changed, 84 insertions(+), 43 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index b681733..0c177e9 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -10,19 +10,21 @@ var metrics *Metrics // Metric is the data points of a single scan type Metric struct { - Scanned int - Updated int - Failed int + Scanned int + Updated int + Deferred int + Failed int } // Metrics is the handler processing all individual scan metrics type Metrics struct { - channel chan *Metric - scanned prometheus.Gauge - updated prometheus.Gauge - failed prometheus.Gauge - total prometheus.Counter - skipped prometheus.Counter + channel chan *Metric + scanned prometheus.Gauge + updated prometheus.Gauge + deferred prometheus.Gauge + failed prometheus.Gauge + total prometheus.Counter + skipped prometheus.Counter } // NewMetric returns a Metric with the counts taken from the appropriate types.Report fields @@ -30,8 +32,9 @@ func NewMetric(report types.Report) *Metric { return &Metric{ Scanned: len(report.Scanned()), // Note: This is for backwards compatibility. ideally, stale containers should be counted separately - Updated: len(report.Updated()) + len(report.Stale()), - Failed: len(report.Failed()), + Updated: len(report.Updated()) + len(report.Stale()), + Deferred: len(report.Deferred()), + Failed: len(report.Failed()), } } @@ -60,6 +63,10 @@ func Default() *Metrics { Name: "watchtower_containers_updated", Help: "Number of containers updated by watchtower during the last scan", }), + deferred: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "watchtower_containers_deferred", + Help: "Number of containers deferred by watchtower during the last scan", + }), failed: promauto.NewGauge(prometheus.GaugeOpts{ Name: "watchtower_containers_failed", Help: "Number of containers where update failed during the last scan", @@ -95,6 +102,7 @@ func (metrics *Metrics) HandleUpdate(channel <-chan *Metric) { metrics.skipped.Inc() metrics.scanned.Set(0) metrics.updated.Set(0) + metrics.deferred.Set(0) metrics.failed.Set(0) continue } @@ -102,6 +110,7 @@ func (metrics *Metrics) HandleUpdate(channel <-chan *Metric) { metrics.total.Inc() metrics.scanned.Set(float64(change.Scanned)) metrics.updated.Set(float64(change.Updated)) + metrics.deferred.Set(float64(change.Deferred)) metrics.failed.Set(float64(change.Failed)) } } diff --git a/pkg/notifications/json.go b/pkg/notifications/json.go index 20da92b..92437eb 100644 --- a/pkg/notifications/json.go +++ b/pkg/notifications/json.go @@ -23,12 +23,13 @@ func (d Data) MarshalJSON() ([]byte, error) { var report jsonMap if d.Report != nil { report = jsonMap{ - `scanned`: marshalReports(d.Report.Scanned()), - `updated`: marshalReports(d.Report.Updated()), - `failed`: marshalReports(d.Report.Failed()), - `skipped`: marshalReports(d.Report.Skipped()), - `stale`: marshalReports(d.Report.Stale()), - `fresh`: marshalReports(d.Report.Fresh()), + `scanned`: marshalReports(d.Report.Scanned()), + `updated`: marshalReports(d.Report.Updated()), + `deferred`: marshalReports(d.Report.Deferred()), + `failed`: marshalReports(d.Report.Failed()), + `skipped`: marshalReports(d.Report.Skipped()), + `stale`: marshalReports(d.Report.Stale()), + `fresh`: marshalReports(d.Report.Fresh()), } } diff --git a/pkg/notifications/json_test.go b/pkg/notifications/json_test.go index ef30c59..fca1231 100644 --- a/pkg/notifications/json_test.go +++ b/pkg/notifications/json_test.go @@ -21,6 +21,7 @@ var _ = Describe("JSON template", func() { ], "host": "Mock", "report": { + "deferred": [], "failed": [ { "currentImageId": "01d210000000", @@ -110,7 +111,7 @@ var _ = Describe("JSON template", func() { }, "title": "Watchtower updates on Mock" }` - data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState) + data := mockDataFromStates(s.UpdatedState, s.DeferredState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState) Expect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected)) }) }) diff --git a/pkg/notifications/preview/data/data.go b/pkg/notifications/preview/data/data.go index 4a002ed..295d097 100644 --- a/pkg/notifications/preview/data/data.go +++ b/pkg/notifications/preview/data/data.go @@ -72,6 +72,8 @@ func (pb *previewData) addContainer(c containerStatus) { pb.report.scanned = append(pb.report.scanned, &c) case UpdatedState: pb.report.updated = append(pb.report.updated, &c) + case DeferredState: + pb.report.deferred = append(pb.report.deferred, &c) case FailedState: pb.report.failed = append(pb.report.failed, &c) case SkippedState: diff --git a/pkg/notifications/preview/data/report.go b/pkg/notifications/preview/data/report.go index 2c8627f..b24674b 100644 --- a/pkg/notifications/preview/data/report.go +++ b/pkg/notifications/preview/data/report.go @@ -10,12 +10,13 @@ import ( type State string const ( - ScannedState State = "scanned" - UpdatedState State = "updated" - FailedState State = "failed" - SkippedState State = "skipped" - StaleState State = "stale" - FreshState State = "fresh" + ScannedState State = "scanned" + UpdatedState State = "updated" + DeferredState State = "deferred" + FailedState State = "failed" + SkippedState State = "skipped" + StaleState State = "stale" + FreshState State = "fresh" ) // StatesFromString parses a string of state characters and returns a slice of the corresponding report states @@ -27,6 +28,8 @@ func StatesFromString(str string) []State { states = append(states, ScannedState) case 'u': states = append(states, UpdatedState) + case 'd': + states = append(states, DeferredState) case 'e': states = append(states, FailedState) case 'k': @@ -43,12 +46,13 @@ func StatesFromString(str string) []State { } type report struct { - scanned []types.ContainerReport - updated []types.ContainerReport - failed []types.ContainerReport - skipped []types.ContainerReport - stale []types.ContainerReport - fresh []types.ContainerReport + scanned []types.ContainerReport + updated []types.ContainerReport + deferred []types.ContainerReport + failed []types.ContainerReport + skipped []types.ContainerReport + stale []types.ContainerReport + fresh []types.ContainerReport } func (r *report) Scanned() []types.ContainerReport { @@ -57,6 +61,9 @@ func (r *report) Scanned() []types.ContainerReport { func (r *report) Updated() []types.ContainerReport { return r.updated } +func (r *report) Deferred() []types.ContainerReport { + return r.deferred +} func (r *report) Failed() []types.ContainerReport { return r.failed } @@ -87,6 +94,7 @@ func (r *report) All() []types.ContainerReport { } appendUnique(r.updated) + appendUnique(r.deferred) appendUnique(r.failed) appendUnique(r.skipped) appendUnique(r.stale) diff --git a/pkg/session/container_status.go b/pkg/session/container_status.go index 8313da1..df10ec1 100644 --- a/pkg/session/container_status.go +++ b/pkg/session/container_status.go @@ -12,6 +12,7 @@ const ( SkippedState ScannedState UpdatedState + DeferredState FailedState FreshState StaleState @@ -70,6 +71,8 @@ func (u *ContainerStatus) State() string { return "Scanned" case UpdatedState: return "Updated" + case DeferredState: + return "Deferred" case FailedState: return "Failed" case FreshState: diff --git a/pkg/session/progress.go b/pkg/session/progress.go index 57069be..0c67c48 100644 --- a/pkg/session/progress.go +++ b/pkg/session/progress.go @@ -50,6 +50,11 @@ func (m Progress) MarkForUpdate(containerID types.ContainerID) { m[containerID].state = UpdatedState } +// MarkForUpdate marks the container identified by containerID for deferral +func (m Progress) MarkDeferred(containerID types.ContainerID) { + m[containerID].state = DeferredState +} + // Report creates a new Report from a Progress instance func (m Progress) Report() types.Report { return NewReport(m) diff --git a/pkg/session/report.go b/pkg/session/report.go index 707eb91..cbcf1db 100644 --- a/pkg/session/report.go +++ b/pkg/session/report.go @@ -7,12 +7,13 @@ import ( ) type report struct { - scanned []types.ContainerReport - updated []types.ContainerReport - failed []types.ContainerReport - skipped []types.ContainerReport - stale []types.ContainerReport - fresh []types.ContainerReport + scanned []types.ContainerReport + updated []types.ContainerReport + deferred []types.ContainerReport + failed []types.ContainerReport + skipped []types.ContainerReport + stale []types.ContainerReport + fresh []types.ContainerReport } func (r *report) Scanned() []types.ContainerReport { @@ -21,6 +22,9 @@ func (r *report) Scanned() []types.ContainerReport { func (r *report) Updated() []types.ContainerReport { return r.updated } +func (r *report) Deferred() []types.ContainerReport { + return r.deferred +} func (r *report) Failed() []types.ContainerReport { return r.failed } @@ -50,6 +54,7 @@ func (r *report) All() []types.ContainerReport { } appendUnique(r.updated) + appendUnique(r.deferred) appendUnique(r.failed) appendUnique(r.skipped) appendUnique(r.stale) @@ -64,12 +69,13 @@ func (r *report) All() []types.ContainerReport { // NewReport creates a types.Report from the supplied Progress func NewReport(progress Progress) types.Report { report := &report{ - scanned: []types.ContainerReport{}, - updated: []types.ContainerReport{}, - failed: []types.ContainerReport{}, - skipped: []types.ContainerReport{}, - stale: []types.ContainerReport{}, - fresh: []types.ContainerReport{}, + scanned: []types.ContainerReport{}, + updated: []types.ContainerReport{}, + deferred: []types.ContainerReport{}, + failed: []types.ContainerReport{}, + skipped: []types.ContainerReport{}, + stale: []types.ContainerReport{}, + fresh: []types.ContainerReport{}, } for _, update := range progress { @@ -88,9 +94,13 @@ func NewReport(progress Progress) types.Report { switch update.state { case UpdatedState: report.updated = append(report.updated, update) + case DeferredState: + report.deferred = append(report.deferred, update) case FailedState: report.failed = append(report.failed, update) default: + // TODO: should this be changed to something lke UnknownState since it shouldn't be possible for a container + // to be stale but its state to not be either UpdatedState, DeferredState, or FailedState? update.state = StaleState report.stale = append(report.stale, update) } @@ -98,6 +108,7 @@ func NewReport(progress Progress) types.Report { sort.Sort(sortableContainers(report.scanned)) sort.Sort(sortableContainers(report.updated)) + sort.Sort(sortableContainers(report.deferred)) sort.Sort(sortableContainers(report.failed)) sort.Sort(sortableContainers(report.skipped)) sort.Sort(sortableContainers(report.stale)) diff --git a/pkg/types/report.go b/pkg/types/report.go index f454fc6..5f6540c 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -4,6 +4,7 @@ package types type Report interface { Scanned() []ContainerReport Updated() []ContainerReport + Deferred() []ContainerReport Failed() []ContainerReport Skipped() []ContainerReport Stale() []ContainerReport diff --git a/tplprev/main.go b/tplprev/main.go index 120f968..6b62d8f 100644 --- a/tplprev/main.go +++ b/tplprev/main.go @@ -18,7 +18,7 @@ func main() { var states string var entries string - flag.StringVar(&states, "states", "cccuuueeekkktttfff", "sCanned, Updated, failEd, sKipped, sTale, Fresh") + flag.StringVar(&states, "states", "cccuuudddeeekkktttfff", "sCanned, Updated, Deferred, failEd, sKipped, sTale, Fresh") flag.StringVar(&entries, "entries", "ewwiiidddd", "Fatal,Error,Warn,Info,Debug,Trace") flag.Parse() From eee431ff259f3a27f794f7547acf300cc5dde68c Mon Sep 17 00:00:00 2001 From: Peter Wilhelm Date: Sun, 7 Jan 2024 18:15:35 -0600 Subject: [PATCH 5/6] Use RFC3339Nano to better align to Docker standard --- internal/actions/mocks/container.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index 86f99b5..bdac6e6 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -82,7 +82,7 @@ func CreateMockContainerWithDigest(id string, name string, image string, created // CreateMockContainerWithDigest should only be used for testing func CreateMockContainerWithImageCreatedTime(id string, name string, image string, created time.Time, imageCreated time.Time) wt.Container { c := CreateMockContainer(id, name, image, created) - c.ImageInfo().Created = imageCreated.UTC().Format(time.RFC3339) + c.ImageInfo().Created = imageCreated.UTC().Format(time.RFC3339Nano) return c } From c37061b3c790b799e4149cd49425b6e031ccaef4 Mon Sep 17 00:00:00 2001 From: Peter Wilhelm Date: Sun, 7 Jan 2024 18:19:00 -0600 Subject: [PATCH 6/6] Update variable name to match the "deferred" language being used elsewhere now --- internal/actions/update.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/actions/update.go b/internal/actions/update.go index 642010f..58f9c40 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -37,7 +37,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e // stale will be true if there is a more recent image than the current container is using stale, newestImage, err := client.IsContainerStale(targetContainer, params) shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params) - imageUpdateDelayed := false + imageUpdateDeferred := false imageAgeDays := 0 if err == nil && shouldUpdate { // Check to make sure we have all the necessary information for recreating the container, including ImageInfo @@ -47,7 +47,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e imageAgeDays, imageErr := getImageAgeDays(targetContainer.ImageInfo().Created) err = imageErr if err == nil { - imageUpdateDelayed = imageAgeDays < params.DeferDays + imageUpdateDeferred = imageAgeDays < params.DeferDays } } } else if log.IsLevelEnabled(log.TraceLevel) { @@ -66,7 +66,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e stale = false staleCheckFailed++ progress.AddSkipped(targetContainer, err) - } else if imageUpdateDelayed { + } else if imageUpdateDeferred { log.Infof("New image found for %s that was created %d day(s) ago but update deferred until %d day(s) after creation", targetContainer.Name(), imageAgeDays, params.DeferDays) // technically the container is stale but we set it to false here because it is this stale flag that tells downstream methods whether to perform the update stale = false