mirror of
https://github.com/moby/moby.git
synced 2026-06-30 19:58:03 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user