From 553420b8f3162b40b30b3ff456f7ff21834adfdc Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 25 Feb 2026 15:14:09 +0100 Subject: [PATCH] daemon/internal/timestamp: replace GetTimestamp for Parse All code in the daemon using `GetTimestamp` called `ParseTimestamp` afterwards to convert the string value to a time.Time. Replace it with a `Parse` function that immediately returns a time.Time (in UTC) so that we don't have to "fuzzy" parse twice. Signed-off-by: Sebastiaan van Stijn --- daemon/containerd/image_list.go | 11 ++----- daemon/events/events_test.go | 20 +++--------- daemon/events/testutils/testutils.go | 11 ++----- daemon/images/image_list.go | 11 +++---- daemon/images/image_prune.go | 8 ++--- daemon/internal/builder-next/builder.go | 11 ++----- daemon/internal/timestamp/timestamp.go | 36 +++++++++------------ daemon/internal/timestamp/timestamp_test.go | 20 ++++-------- daemon/network/filter.go | 10 ++---- daemon/prune.go | 16 +++------ 10 files changed, 48 insertions(+), 106 deletions(-) diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go index 83578d54cb..5bac143cf4 100644 --- a/daemon/containerd/image_list.go +++ b/daemon/containerd/image_list.go @@ -601,20 +601,15 @@ func (i *ImageService) setupFilters(ctx context.Context, imageFilters filters.Ar return nil, err } + now := time.Now() err = imageFilters.WalkValues("until", func(value string) error { - ts, err := timestamp.GetTimestamp(value, time.Now()) + until, err := timestamp.Parse(value, now) if err != nil { return errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err)) } - seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0) - if err != nil { - return errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err)) - } - until := time.Unix(seconds, nanoseconds) fltrs = append(fltrs, func(image c8dimages.Image) bool { - created := image.CreatedAt - return created.Before(until) + return image.CreatedAt.Before(until) }) return nil }) diff --git a/daemon/events/events_test.go b/daemon/events/events_test.go index e88ee1f997..bdf6e671a2 100644 --- a/daemon/events/events_test.go +++ b/daemon/events/events_test.go @@ -126,10 +126,7 @@ func TestLogEvents(t *testing.T) { // 2016-03-07T17:28:03.129014751+02:00 container destroy 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover) func TestLoadBufferedEvents(t *testing.T) { now := time.Now() - f, err := timestamp.GetTimestamp("2016-03-07T17:28:03.100000000+02:00", now) - assert.NilError(t, err) - - s, sNano, err := timestamp.ParseTimestamps(f, -1) + since, err := timestamp.Parse("2016-03-07T17:28:03.100000000+02:00", now) assert.NilError(t, err) m1, err := eventstestutils.Scan("2016-03-07T17:28:03.022433271+02:00 container die 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)") @@ -145,7 +142,6 @@ func TestLoadBufferedEvents(t *testing.T) { events: []events.Message{*m1, *m2, *m3}, } - since := time.Unix(s, sNano) until := time.Time{} messages := evts.loadBufferedEvents(since, until, nil) @@ -154,16 +150,11 @@ func TestLoadBufferedEvents(t *testing.T) { func TestLoadBufferedEventsOnlyFromPast(t *testing.T) { now := time.Now() - f, err := timestamp.GetTimestamp("2016-03-07T17:28:03.090000000+02:00", now) + + since, err := timestamp.Parse("2016-03-07T17:28:03.090000000+02:00", now) assert.NilError(t, err) - s, sNano, err := timestamp.ParseTimestamps(f, 0) - assert.NilError(t, err) - - f, err = timestamp.GetTimestamp("2016-03-07T17:28:03.100000000+02:00", now) - assert.NilError(t, err) - - u, uNano, err := timestamp.ParseTimestamps(f, 0) + until, err := timestamp.Parse("2016-03-07T17:28:03.100000000+02:00", now) assert.NilError(t, err) m1, err := eventstestutils.Scan("2016-03-07T17:28:03.022433271+02:00 container die 0b863f2a26c18557fc6cdadda007c459f9ec81b874780808138aea78a3595079 (image=ubuntu, name=small_hoover)") @@ -179,9 +170,6 @@ func TestLoadBufferedEventsOnlyFromPast(t *testing.T) { events: []events.Message{*m1, *m2, *m3}, } - since := time.Unix(s, sNano) - until := time.Unix(u, uNano) - messages := evts.loadBufferedEvents(since, until, nil) assert.Assert(t, is.Len(messages, 1)) assert.Check(t, is.Equal(messages[0].Type, events.NetworkEventType)) diff --git a/daemon/events/testutils/testutils.go b/daemon/events/testutils/testutils.go index 282cdc3274..d392669eca 100644 --- a/daemon/events/testutils/testutils.go +++ b/daemon/events/testutils/testutils.go @@ -45,12 +45,7 @@ func Scan(text string) (*events.Message, error) { return nil, fmt.Errorf("text is not an event: %s", text) } - f, err := timestamp.GetTimestamp(md["timestamp"], time.Now()) - if err != nil { - return nil, err - } - - t, tn, err := timestamp.ParseTimestamps(f, -1) + created, err := timestamp.Parse(md["timestamp"], time.Now()) if err != nil { return nil, err } @@ -62,8 +57,8 @@ func Scan(text string) (*events.Message, error) { } return &events.Message{ - Time: t, - TimeNano: time.Unix(t, tn).UnixNano(), + Time: created.Unix(), + TimeNano: created.UnixNano(), Type: events.Type(md["eventType"]), Action: events.Action(md["action"]), Actor: events.Actor{ diff --git a/daemon/images/image_list.go b/daemon/images/image_list.go index d750aebf67..e8e3919505 100644 --- a/daemon/images/image_list.go +++ b/daemon/images/image_list.go @@ -62,17 +62,14 @@ func (i *ImageService) Images(ctx context.Context, opts imagebackend.ListOptions return nil, err } + now := time.Now() err = opts.Filters.WalkValues("until", func(value string) error { - ts, err := timestamp.GetTimestamp(value, time.Now()) + ts, err := timestamp.Parse(value, now) if err != nil { return errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err)) } - seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0) - if err != nil { - return errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err)) - } - if tsUnix := time.Unix(seconds, nanoseconds); beforeFilter.IsZero() || beforeFilter.After(tsUnix) { - beforeFilter = tsUnix + if beforeFilter.IsZero() || beforeFilter.After(ts) { + beforeFilter = ts } return nil }) diff --git a/daemon/images/image_prune.go b/daemon/images/image_prune.go index 102b33e3cb..d56cab634b 100644 --- a/daemon/images/image_prune.go +++ b/daemon/images/image_prune.go @@ -193,13 +193,9 @@ func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) { if len(untilFilters) > 1 { return time.Time{}, errdefs.InvalidParameter(errors.New("more than one until filter specified")) } - ts, err := timestamp.GetTimestamp(untilFilters[0], time.Now()) + t, err := timestamp.Parse(untilFilters[0], time.Now()) if err != nil { return time.Time{}, errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err)) } - seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0) - if err != nil { - return time.Time{}, errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err)) - } - return time.Unix(seconds, nanoseconds), nil + return t, nil } diff --git a/daemon/internal/builder-next/builder.go b/daemon/internal/builder-next/builder.go index 58a616026c..30f2b80467 100644 --- a/daemon/internal/builder-next/builder.go +++ b/daemon/internal/builder-next/builder.go @@ -674,20 +674,13 @@ func toBuildkitPruneInfo(opts buildbackend.CachePruneOptions) (client.PruneInfo, case 0: // nothing to do case 1: - ts, err := timestamp.GetTimestamp(untilValues[0], time.Now()) + parsed, err := timestamp.Parse(untilValues[0], time.Now()) if err != nil { return client.PruneInfo{}, errInvalidFilterValue{ errors.Wrapf(err, "%q filter expects a duration (e.g., '24h') or a timestamp", filterKey), } } - seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0) - if err != nil { - return client.PruneInfo{}, errInvalidFilterValue{ - errors.Wrapf(err, "failed to parse timestamp %q", ts), - } - } - - until = time.Since(time.Unix(seconds, nanoseconds)) + until = time.Since(parsed) default: return client.PruneInfo{}, errMultipleFilterValues{} } diff --git a/daemon/internal/timestamp/timestamp.go b/daemon/internal/timestamp/timestamp.go index 2496f68483..be6848022a 100644 --- a/daemon/internal/timestamp/timestamp.go +++ b/daemon/internal/timestamp/timestamp.go @@ -17,19 +17,17 @@ const ( dateLocal = "2006-01-02" // RFC3339 with local timezone and time at 00:00:00 ) -// GetTimestamp tries to parse given string as golang duration, -// then RFC3339 time and finally as a Unix timestamp. If -// any of these were successful, it returns a Unix timestamp -// as string otherwise returns the given value back. -// In case of duration input, the returned timestamp is computed -// as the given reference time minus the amount of the duration. -func GetTimestamp(value string, reference time.Time) (string, error) { +// Parse tries to parse given string as golang duration, then RFC3339 time and +// finally as a Unix timestamp. The returned time is normalized to UTC. +// +// In case of duration input, the returned timestamp is computed as the given +// reference time minus the amount of the duration. +func Parse(value string, reference time.Time) (time.Time, error) { + if strings.TrimSpace(value) == "" { + return time.Time{}, errors.New("failed to parse value as time or duration: value is empty") + } if d, err := time.ParseDuration(value); value != "0" && err == nil { - t := reference.Add(-d) - if t.Nanosecond() == 0 { - return strconv.FormatInt(t.Unix(), 10), nil - } - return fmt.Sprintf("%d.%09d", t.Unix(), t.Nanosecond()), nil + return reference.Add(-d).UTC(), nil } var format string @@ -88,18 +86,16 @@ func GetTimestamp(value string, reference time.Time) (string, error) { if err != nil { // if there is a `-` then it's an RFC3339 like timestamp if strings.Contains(value, "-") { - return "", err // was probably an RFC3339 like timestamp but the parser failed with an error + return time.Time{}, err // was probably an RFC3339 like timestamp but the parser failed with an error } - if _, _, err := parseTimestamp(value); err != nil { - return "", fmt.Errorf("failed to parse value as time or duration: %q", value) + sec, nsec, err := parseTimestamp(value) + if err != nil { + return time.Time{}, fmt.Errorf("failed to parse value as time or duration: %q", value) } - return value, nil // unix timestamp in and out case (meaning: the value passed at the command line is already in the right format for passing to the server) + return time.Unix(sec, nsec), nil } - if t.Nanosecond() == 0 { - return strconv.FormatInt(t.Unix(), 10), nil - } - return fmt.Sprintf("%d.%09d", t.Unix(), t.Nanosecond()), nil + return t.UTC(), nil } // ParseTimestamps returns seconds and nanoseconds from a timestamp that has diff --git a/daemon/internal/timestamp/timestamp_test.go b/daemon/internal/timestamp/timestamp_test.go index ae71cf834b..2b009a4d3c 100644 --- a/daemon/internal/timestamp/timestamp_test.go +++ b/daemon/internal/timestamp/timestamp_test.go @@ -1,8 +1,6 @@ package timestamp_test import ( - "fmt" - "strconv" "testing" "time" @@ -11,7 +9,7 @@ import ( is "gotest.tools/v3/assert/cmp" ) -func TestGetTimestamp(t *testing.T) { +func TestParse(t *testing.T) { now := time.Date(2020, 1, 2, 3, 4, 5, 123456789, time.UTC) tests := []struct { in string @@ -73,26 +71,20 @@ func TestGetTimestamp(t *testing.T) { name = "" } t.Run(name, func(t *testing.T) { - out, err := timestamp.GetTimestamp(tc.in, now) + out, err := timestamp.Parse(tc.in, now) if tc.expectedErr { assert.Assert(t, err != nil, "expected error for %q, got none", tc.in) return } assert.NilError(t, err) - exp, err := time.Parse(time.RFC3339Nano, tc.expected) + want, err := time.Parse(time.RFC3339Nano, tc.expected) assert.NilError(t, err, "invalid expected value") - var want string - if exp.Nanosecond() == 0 { - want = strconv.FormatInt(exp.Unix(), 10) - } else { - want = fmt.Sprintf("%d.%09d", exp.Unix(), exp.Nanosecond()) - } - assert.Assert(t, out == want, + assert.Assert(t, out.Equal(want), "expected: %s\ngot: %s", - want, - out, + want.Format(time.RFC3339Nano), + out.Format(time.RFC3339Nano), ) }) } diff --git a/daemon/network/filter.go b/daemon/network/filter.go index 5cca048b90..66015f8bf6 100644 --- a/daemon/network/filter.go +++ b/daemon/network/filter.go @@ -96,20 +96,16 @@ func newFilter(args filters.Args) (Filter, error) { if err := args.WalkValues("type", validateNetworkTypeFilter); err != nil { return Filter{}, err } - until := time.Time{} + var until time.Time if untilFilters := args.Get("until"); len(untilFilters) > 0 { if len(untilFilters) > 1 { return Filter{}, errdefs.InvalidParameter(errors.New("more than one until filter specified")) } - ts, err := timestamp.GetTimestamp(untilFilters[0], time.Now()) + var err error + until, err = timestamp.Parse(untilFilters[0], time.Now()) if err != nil { return Filter{}, errdefs.InvalidParameter(err) } - seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0) - if err != nil { - return Filter{}, errdefs.InvalidParameter(err) - } - until = time.Unix(seconds, nanoseconds) } return Filter{ args: args, diff --git a/daemon/prune.go b/daemon/prune.go index e2336f5326..7dd21d5a79 100644 --- a/daemon/prune.go +++ b/daemon/prune.go @@ -205,24 +205,18 @@ func (daemon *Daemon) NetworkPrune(ctx context.Context, filterArgs filters.Args) } func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) { - until := time.Time{} if !pruneFilters.Contains("until") { - return until, nil + return time.Time{}, nil } untilFilters := pruneFilters.Get("until") if len(untilFilters) > 1 { - return until, errdefs.InvalidParameter(errors.New("more than one until filter specified")) + return time.Time{}, errdefs.InvalidParameter(errors.New("more than one until filter specified")) } - ts, err := timestamp.GetTimestamp(untilFilters[0], time.Now()) + t, err := timestamp.Parse(untilFilters[0], time.Now()) if err != nil { - return until, errdefs.InvalidParameter(err) + return time.Time{}, errdefs.InvalidParameter(err) } - seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0) - if err != nil { - return until, errdefs.InvalidParameter(err) - } - until = time.Unix(seconds, nanoseconds) - return until, nil + return t, nil } func matchLabels(pruneFilters filters.Args, labels map[string]string) bool {