Merge pull request #52094 from thaJeztah/backend_log_opts

daemon/server: improve parsing timestamps and move it to the router
This commit is contained in:
Paweł Gronowski
2026-04-01 13:17:42 +02:00
committed by GitHub
18 changed files with 278 additions and 243 deletions

View File

@@ -526,10 +526,7 @@ func (c *containerAdapter) logs(ctx context.Context, options api.LogSubscription
if err != nil {
return nil, err
}
// print since as this formatted string because the docker container
// logs interface expects it like this.
// see [github.com/moby/moby/v2/daemon/internal/timestamp.ParseTimestamps]
apiOptions.Since = fmt.Sprintf("%d.%09d", since.Unix(), int64(since.Nanosecond()))
apiOptions.Since = since
}
if options.Tail < 0 {

View File

@@ -6,7 +6,6 @@ import (
"io"
"os"
"strconv"
"time"
"github.com/containerd/log"
"github.com/distribution/reference"
@@ -15,7 +14,6 @@ import (
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/v2/daemon/cluster/convert"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"github.com/moby/moby/v2/daemon/server/backend"
"github.com/moby/moby/v2/daemon/server/swarmbackend"
"github.com/moby/moby/v2/errdefs"
@@ -460,13 +458,9 @@ func (c *Cluster) ServiceLogs(ctx context.Context, selector *backend.LogSelector
// get the since value - the time in the past we're looking at logs starting from
var sinceProto *gogotypes.Timestamp
if config.Since != "" {
s, n, err := timestamp.ParseTimestamps(config.Since, 0)
if err != nil {
return nil, errors.Wrap(err, "could not parse since timestamp")
}
since := time.Unix(s, n)
sinceProto, err = gogotypes.TimestampProto(since)
if !config.Since.IsZero() {
var err error
sinceProto, err = gogotypes.TimestampProto(config.Since)
if err != nil {
return nil, errors.Wrap(err, "could not parse timestamp to proto")
}

View File

@@ -3,6 +3,7 @@ package containerd
import (
"context"
"encoding/json"
"fmt"
"runtime"
"sort"
"strings"
@@ -600,22 +601,17 @@ 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 err
return errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err))
}
seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0)
if err != nil {
return 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 err
return nil
})
if err != nil {
return nil, err

View File

@@ -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))

View File

@@ -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{

View File

@@ -14,6 +14,7 @@ import (
"github.com/moby/moby/v2/daemon/internal/layer"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"github.com/moby/moby/v2/daemon/server/imagebackend"
"github.com/moby/moby/v2/errdefs"
)
var acceptedImageFilterTags = map[string]bool{
@@ -61,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 err
return errdefs.InvalidParameter(fmt.Errorf("invalid value for 'until' filter: %w", err))
}
seconds, nanoseconds, err := timestamp.ParseTimestamps(ts, 0)
if err != nil {
return err
}
if tsUnix := time.Unix(seconds, nanoseconds); beforeFilter.IsZero() || beforeFilter.After(tsUnix) {
beforeFilter = tsUnix
if beforeFilter.IsZero() || beforeFilter.After(ts) {
beforeFilter = ts
}
return nil
})

View File

@@ -2,6 +2,7 @@ package images
import (
"context"
"fmt"
"strconv"
"time"
@@ -185,22 +186,16 @@ func matchLabels(pruneFilters filters.Args, labels map[string]string) bool {
}
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, 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, err
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 until, err
}
until = time.Unix(seconds, nanoseconds)
return until, nil
return t, nil
}

View File

@@ -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{}
}

View File

@@ -1,8 +1,8 @@
package timestamp
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"time"
@@ -17,15 +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 {
return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil
return reference.Add(-d).UTC(), nil
}
var format string
@@ -84,48 +86,83 @@ 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)
t, err = parseTimestamp(value)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse value as time or duration: %w", err)
}
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 fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
return t.UTC(), nil
}
// ParseTimestamps returns seconds and nanoseconds from a timestamp that has
// the format ("%d.%09d", time.Unix(), int64(time.Nanosecond())).
// If the incoming nanosecond portion is longer than 9 digits it is truncated.
// The expectation is that the seconds and nanoseconds will be used to create a
// time variable. For example:
// ParseUnixTimestamp parses a UNIX timestamp with optional nanoseconds
// and returns it as a [time.Time].
//
// seconds, nanoseconds, _ := ParseTimestamp("1136073600.000000001",0)
// since := time.Unix(seconds, nanoseconds)
// Value should be formatted as
//
// returns seconds as defaultSeconds if value == ""
func ParseTimestamps(value string, defaultSeconds int64) (seconds int64, nanoseconds int64, _ error) {
// "%d.%09d" (seconds.nanoseconds)
//
// It accepts either:
// - "seconds"
// - "seconds.nanoseconds"
//
// If the nanoseconds has less than 9 digits it is right-padded with zeros.
// If it has more than 9 digits it is truncated.
//
// Empty values ("") produce a zero-value, with no error.
func ParseUnixTimestamp(value string) (time.Time, error) {
if value == "" {
return defaultSeconds, 0, nil
return time.Time{}, nil
}
return parseTimestamp(value)
t, err := parseTimestamp(value)
if err != nil {
return time.Time{}, fmt.Errorf("invalid timestamp %q: %w", value, err)
}
return t, nil
}
func parseTimestamp(value string) (seconds int64, nanoseconds int64, _ error) {
func parseTimestamp(value string) (time.Time, error) {
s, n, ok := strings.Cut(value, ".")
sec, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return sec, 0, err
var numErr *strconv.NumError
if errors.As(err, &numErr) {
err = numErr.Err
}
return time.Time{}, fmt.Errorf("invalid seconds %q: %w", s, err)
}
if !ok {
return sec, 0, nil
if !ok || n == "" || n == "000000000" || n == "0" {
// Fast path for "zero" nanoseconds
return time.Unix(sec, 0).UTC(), nil
}
// The documented format is 9 digits. Historically we allowed more and
// truncated to nanoseconds precision. Let's add some sensible limit;
// 20 digits would be a full uint64 value.
const maxFracDigits = 20
if len(n) > maxFracDigits {
return time.Time{}, fmt.Errorf("invalid nanoseconds: length %d exceeds maximum %d", len(n), maxFracDigits)
}
// Nanoseconds must be digits only (reject '+' / '-' and other junk).
for i := range len(n) {
if c := n[i]; c < '0' || c > '9' {
return time.Time{}, fmt.Errorf("invalid nanoseconds: invalid character %q at position %d", c, i)
}
}
// cap at 9 digits, or pad to 9 digits
if len(n) > 9 {
n = n[:9]
} else if len(n) < 9 {
n += strings.Repeat("0", 9-len(n))
}
nsec, err := strconv.ParseInt(n, 10, 64)
if err != nil {
return sec, nsec, err
// this should never fail, but just in case
return time.Time{}, fmt.Errorf("invalid nanoseconds %q: %w", n, err)
}
// should already be in nanoseconds but just in case convert n to nanoseconds
nsec = int64(float64(nsec) * math.Pow(float64(10), float64(9-len(n))))
return sec, nsec, nil
return time.Unix(sec, nsec).UTC(), nil
}

View File

@@ -1,95 +1,143 @@
package timestamp
package timestamp_test
import (
"fmt"
"testing"
"time"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestGetTimestamp(t *testing.T) {
now := time.Now().In(time.UTC)
cases := []struct {
in, expected string
expectedErr bool
func TestParse(t *testing.T) {
now := time.Date(2020, 1, 2, 3, 4, 5, 123456789, time.UTC)
tests := []struct {
in string
expected string // RFC3339Nano in UTC
expectedErr bool
}{
// Partial RFC3339 strings get parsed with second precision
{"2006-01-02T15:04:05.999999999+07:00", "1136189045.999999999", false},
{"2006-01-02T15:04:05.999999999Z", "1136214245.999999999", false},
{"2006-01-02T15:04:05.999999999", "1136214245.999999999", false},
{"2006-01-02T15:04:05Z", "1136214245.000000000", false},
{"2006-01-02T15:04:05", "1136214245.000000000", false},
{"2006-01-02T15:04:0Z", "", true},
{"2006-01-02T15:04:0", "", true},
{"2006-01-02T15:04Z", "1136214240.000000000", false},
{"2006-01-02T15:04+00:00", "1136214240.000000000", false},
{"2006-01-02T15:04-00:00", "1136214240.000000000", false},
{"2006-01-02T15:04", "1136214240.000000000", false},
{"2006-01-02T15:0Z", "", true},
{"2006-01-02T15:0", "", true},
{"2006-01-02T15Z", "1136214000.000000000", false},
{"2006-01-02T15+00:00", "1136214000.000000000", false},
{"2006-01-02T15-00:00", "1136214000.000000000", false},
{"2006-01-02T15", "1136214000.000000000", false},
{"2006-01-02T1Z", "1136163600.000000000", false},
{"2006-01-02T1", "1136163600.000000000", false},
{"2006-01-02TZ", "", true},
{"2006-01-02T", "", true},
{"2006-01-02+00:00", "1136160000.000000000", false},
{"2006-01-02-00:00", "1136160000.000000000", false},
{"2006-01-02-00:01", "1136160060.000000000", false},
{"2006-01-02Z", "1136160000.000000000", false},
{"2006-01-02", "1136160000.000000000", false},
{"2015-05-13T20:39:09Z", "1431549549.000000000", false},
{in: "2006-01-02T15:04:05.999999999+07:00", expected: "2006-01-02T08:04:05.999999999Z"},
{in: "2006-01-02T15:04:05.999999999Z", expected: "2006-01-02T15:04:05.999999999Z"},
{in: "2006-01-02T15:04:05.999999999", expected: "2006-01-02T15:04:05.999999999Z"},
{in: "2006-01-02T15:04:05Z", expected: "2006-01-02T15:04:05Z"},
{in: "2006-01-02T15:04:05", expected: "2006-01-02T15:04:05Z"},
{in: "2006-01-02T15:04:0Z", expectedErr: true},
{in: "2006-01-02T15:04:0", expectedErr: true},
{in: "2006-01-02T15:04Z", expected: "2006-01-02T15:04:00Z"},
{in: "2006-01-02T15:04+00:00", expected: "2006-01-02T15:04:00Z"},
{in: "2006-01-02T15:04-00:00", expected: "2006-01-02T15:04:00Z"},
{in: "2006-01-02T15:04", expected: "2006-01-02T15:04:00Z"},
{in: "2006-01-02T15:0Z", expectedErr: true},
{in: "2006-01-02T15:0", expectedErr: true},
{in: "2006-01-02T15Z", expected: "2006-01-02T15:00:00Z"},
{in: "2006-01-02T15+00:00", expected: "2006-01-02T15:00:00Z"},
{in: "2006-01-02T15-00:00", expected: "2006-01-02T15:00:00Z"},
{in: "2006-01-02T15", expected: "2006-01-02T15:00:00Z"},
{in: "2006-01-02T1Z", expected: "2006-01-02T01:00:00Z"},
{in: "2006-01-02T1", expected: "2006-01-02T01:00:00Z"},
{in: "2006-01-02TZ", expectedErr: true},
{in: "2006-01-02T", expectedErr: true},
{in: "2006-01-02+00:00", expected: "2006-01-02T00:00:00Z"},
{in: "2006-01-02-00:00", expected: "2006-01-02T00:00:00Z"},
{in: "2006-01-02-00:01", expected: "2006-01-02T00:01:00Z"},
{in: "2006-01-02Z", expected: "2006-01-02T00:00:00Z"},
{in: "2006-01-02", expected: "2006-01-02T00:00:00Z"},
{in: "2015-05-13T20:39:09Z", expected: "2015-05-13T20:39:09Z"},
// unix timestamps returned as is
{"1136073600", "1136073600", false},
{"1136073600.000000001", "1136073600.000000001", false},
// Durations
{"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix()), false},
{"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
{"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
// Unix timestamps
{in: "1136073600", expected: "2006-01-01T00:00:00Z"},
{in: "1136073600.000000001", expected: "2006-01-01T00:00:00.000000001Z"},
{in: "1136073600.00000000000000000001", expected: "2006-01-01T00:00:00Z"}, // max length
{in: "1136073600.000000000000000000001", expectedErr: true}, // too long
{"invalid", "", true},
{"", "", true},
// Durations (relative to `now`)
{in: "-1m", expected: "2020-01-02T03:05:05.123456789Z"}, // welcome to the future ¯\_(ツ)_/¯
{in: "0m", expected: "2020-01-02T03:04:05.123456789Z"},
{in: "1m", expected: "2020-01-02T03:03:05.123456789Z"},
{in: "1.5h", expected: "2020-01-02T01:34:05.123456789Z"},
{in: "1h30m", expected: "2020-01-02T01:34:05.123456789Z"},
// invalid values
{in: " 1136073600 \t", expectedErr: true},
{in: "foo.bar", expectedErr: true},
{in: "1136073600.bar", expectedErr: true},
{in: "1136073600.000000001bar", expectedErr: true},
{in: "invalid", expectedErr: true},
{in: "", expectedErr: true},
}
for _, c := range cases {
o, err := GetTimestamp(c.in, now)
if o != c.expected ||
(err == nil && c.expectedErr) ||
(err != nil && !c.expectedErr) {
t.Errorf("wrong value for '%s'. expected:'%s' got:'%s' with error: `%s`", c.in, c.expected, o, err)
t.Fail()
for _, tc := range tests {
name := tc.in
if name == "" {
name = "<empty>"
}
t.Run(name, func(t *testing.T) {
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)
want, err := time.Parse(time.RFC3339Nano, tc.expected)
assert.NilError(t, err, "invalid expected value")
assert.Assert(t, out.Equal(want),
"expected: %s\ngot: %s",
want.Format(time.RFC3339Nano),
out.Format(time.RFC3339Nano),
)
})
}
}
func TestParseTimestamps(t *testing.T) {
cases := []struct {
in string
def, expectedS, expectedN int64
expectedErr bool
func TestParseUnixTimestamp(t *testing.T) {
tests := []struct {
in string
expectedS int64
expectedN int64
expectedErr bool
}{
// unix timestamps
{"1136073600", 0, 1136073600, 0, false},
{"1136073600.000000001", 0, 1136073600, 1, false},
{"1136073600.0000000010", 0, 1136073600, 1, false},
{"1136073600.0000000001", 0, 1136073600, 0, false},
{"1136073600.0000000009", 0, 1136073600, 0, false},
{"1136073600.00000001", 0, 1136073600, 10, false},
{"foo.bar", 0, 0, 0, true},
{"1136073600.bar", 0, 1136073600, 0, true},
{"", -1, -1, 0, false},
{in: "1136073600", expectedS: 1136073600, expectedN: 0},
{in: "1136073600.", expectedS: 1136073600, expectedN: 0}, // allow empty nanoseconds
{in: "1136073600.0", expectedS: 1136073600, expectedN: 0},
{in: "1136073600.000000001", expectedS: 1136073600, expectedN: 1},
{in: "1136073600.0000000010", expectedS: 1136073600, expectedN: 1}, // truncates
{in: "1136073600.0000000001", expectedS: 1136073600, expectedN: 0}, // truncates
{in: "1136073600.0000000009", expectedS: 1136073600, expectedN: 0}, // truncates
{in: "1136073600.00000001", expectedS: 1136073600, expectedN: 10}, // pads
{in: "1136073600.1", expectedS: 1136073600, expectedN: 100000000}, // pads
// invalid values
{in: " 1136073600 \t", expectedErr: true},
{in: "foo.bar", expectedErr: true},
{in: ".0000000009", expectedErr: true},
{in: "1136073600.bar", expectedErr: true},
// empty value
{in: ""},
}
for _, c := range cases {
s, n, err := ParseTimestamps(c.in, c.def)
if s != c.expectedS ||
n != c.expectedN ||
(err == nil && c.expectedErr) ||
(err != nil && !c.expectedErr) {
t.Errorf("wrong values for input `%s` with default `%d` expected:'%d'seconds and `%d`nanosecond got:'%d'seconds and `%d`nanoseconds with error: `%s`", c.in, c.def, c.expectedS, c.expectedN, s, n, err)
t.Fail()
for _, tc := range tests {
name := tc.in
if name == "" {
name = "<empty>"
}
t.Run(name, func(t *testing.T) {
out, err := timestamp.ParseUnixTimestamp(tc.in)
if tc.expectedErr {
assert.Assert(t, err != nil, "expected error for %q, got none", tc.in)
return
}
assert.NilError(t, err)
if tc.in == "" {
assert.Assert(t, out.IsZero())
return
}
assert.Check(t, is.Equal(out.Unix(), tc.expectedS))
assert.Check(t, is.Equal(int64(out.Nanosecond()), tc.expectedN))
})
}
}

View File

@@ -3,14 +3,12 @@ package daemon
import (
"context"
"strconv"
"time"
"github.com/containerd/containerd/v2/pkg/tracing"
"github.com/containerd/log"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/v2/daemon/config"
"github.com/moby/moby/v2/daemon/container"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"github.com/moby/moby/v2/daemon/logger"
logcache "github.com/moby/moby/v2/daemon/logger/loggerutils/cache"
"github.com/moby/moby/v2/daemon/server/backend"
@@ -77,28 +75,10 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c
tailLines = -1
}
var since time.Time
if config.Since != "" {
s, n, err := timestamp.ParseTimestamps(config.Since, 0)
if err != nil {
return nil, false, err
}
since = time.Unix(s, n)
}
var until time.Time
if config.Until != "" && config.Until != "0" {
s, n, err := timestamp.ParseTimestamps(config.Until, 0)
if err != nil {
return nil, false, err
}
until = time.Unix(s, n)
}
follow := config.Follow && !cLogCreated
logs := logReader.ReadLogs(ctx, logger.ReadConfig{
Since: since,
Until: until,
Since: config.Since,
Until: config.Until,
Tail: tailLines,
Follow: follow,
})

View File

@@ -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,

View File

@@ -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 {

View File

@@ -112,8 +112,8 @@ type ContainerListOptions struct {
type ContainerLogsOptions struct {
ShowStdout bool
ShowStderr bool
Since string
Until string
Since time.Time
Until time.Time
Timestamps bool
Follow bool
Tail string

View File

@@ -11,6 +11,7 @@ import (
"slices"
"strconv"
"strings"
"time"
"github.com/containerd/log"
"github.com/containerd/platforms"
@@ -20,6 +21,7 @@ import (
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/v2/daemon/internal/filters"
"github.com/moby/moby/v2/daemon/internal/runconfig"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"github.com/moby/moby/v2/daemon/internal/versions"
"github.com/moby/moby/v2/daemon/libnetwork/netlabel"
networkSettings "github.com/moby/moby/v2/daemon/network"
@@ -192,15 +194,33 @@ func (c *containerRouter) getContainersLogs(ctx context.Context, w http.Response
// with the appropriate status code.
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
if !stdout && !stderr {
return errdefs.InvalidParameter(errors.New("Bad parameters: you must choose at least one stream"))
return errdefs.InvalidParameter(errors.New("must specify at least one of 'stdout' or 'stderr'"))
}
var since time.Time
if v := r.Form.Get("since"); v != "" {
var err error
since, err = timestamp.ParseUnixTimestamp(v)
if err != nil {
return errdefs.InvalidParameter(fmt.Errorf(`invalid value for "since": %w`, err))
}
}
var until time.Time
if v := r.Form.Get("until"); v != "" && v != "0" {
var err error
until, err = timestamp.ParseUnixTimestamp(v)
if err != nil {
return errdefs.InvalidParameter(fmt.Errorf(`invalid value for "until": %w`, err))
}
}
containerName := vars["name"]
logsConfig := &backend.ContainerLogsOptions{
Follow: httputils.BoolValue(r, "follow"),
Timestamps: httputils.BoolValue(r, "timestamps"),
Since: r.Form.Get("since"),
Until: r.Form.Get("until"),
Since: since,
Until: until,
Tail: r.Form.Get("tail"),
ShowStdout: stdout,
ShowStderr: stderr,

View File

@@ -3,14 +3,18 @@ package swarm
import (
"context"
"errors"
"fmt"
"net/http"
"time"
basictypes "github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/v2/daemon/internal/timestamp"
"github.com/moby/moby/v2/daemon/internal/versions"
"github.com/moby/moby/v2/daemon/server/backend"
"github.com/moby/moby/v2/daemon/server/httputils"
"github.com/moby/moby/v2/daemon/server/httputils/logstream"
"github.com/moby/moby/v2/errdefs"
)
// swarmLogs takes an http response, request, and selector, and writes the logs
@@ -23,7 +27,16 @@ func (sr *swarmRouter) swarmLogs(ctx context.Context, w http.ResponseWriter, r *
// with the appropriate status code.
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
if !stdout && !stderr {
return errors.New("Bad parameters: you must choose at least one stream")
return errdefs.InvalidParameter(errors.New("must specify at least one of 'stdout' or 'stderr'"))
}
var since time.Time
if s := r.Form.Get("since"); s != "" {
var err error
since, err = timestamp.ParseUnixTimestamp(s)
if err != nil {
return errdefs.InvalidParameter(fmt.Errorf(`invalid value for "since": %w`, err))
}
}
// there is probably a neater way to manufacture the LogsOptions
@@ -31,7 +44,7 @@ func (sr *swarmRouter) swarmLogs(ctx context.Context, w http.ResponseWriter, r *
logsConfig := &backend.ContainerLogsOptions{
Follow: httputils.BoolValue(r, "follow"),
Timestamps: httputils.BoolValue(r, "timestamps"),
Since: r.Form.Get("since"),
Since: since,
Tail: r.Form.Get("tail"),
ShowStdout: stdout,
ShowStderr: stderr,

View File

@@ -295,13 +295,13 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *
return err
}
since, err := eventTime(r.Form.Get("since"))
since, err := timestamp.ParseUnixTimestamp(r.Form.Get("since"))
if err != nil {
return err
return invalidRequestError{fmt.Errorf("invalid value for 'since': %w", err)}
}
until, err := eventTime(r.Form.Get("until"))
until, err := timestamp.ParseUnixTimestamp(r.Form.Get("until"))
if err != nil {
return err
return invalidRequestError{fmt.Errorf("invalid value for 'until': %w", err)}
}
var (
@@ -430,17 +430,6 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h
})
}
func eventTime(formTime string) (time.Time, error) {
t, tNano, err := timestamp.ParseTimestamps(formTime, -1)
if err != nil {
return time.Time{}, err
}
if t == -1 {
return time.Time{}, nil
}
return time.Unix(t, tNano), nil
}
// These fields were deprecated in docker v1.10, API v1.22, but not removed
// from the API responses. Unfortunately, the Docker CLI (and compose indirectly),
// continued using these fields up until v25.0.0, and panic if the fields are

View File

@@ -11,6 +11,7 @@ import (
"testing"
"time"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/integration-cli/cli"
@@ -63,7 +64,8 @@ func (s *DockerAPISuite) TestLogsAPINoStdoutNorStderr(c *testing.T) {
defer apiClient.Close()
_, err = apiClient.ContainerLogs(testutil.GetContext(c), name, client.ContainerLogsOptions{})
assert.ErrorContains(c, err, "Bad parameters: you must choose at least one stream")
assert.ErrorType(c, err, cerrdefs.IsInvalidArgument)
assert.ErrorContains(c, err, "must specify at least one of 'stdout' or 'stderr'")
}
// Regression test for #12704