Browse code

Copy the daemon/internal/timestamp package to internal client package

This change copies the daemon/internal/timestamp package (previously api/types/time) to an internal client package and updates the client usage for GetTimestamp functionality.

Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>

Austin Vazquez authored on 2025/08/14 05:45:11
Showing 13 changed files
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"time"
9 9
 
10 10
 	"github.com/moby/moby/api/types/container"
11
-	timetypes "github.com/moby/moby/api/types/time"
11
+	"github.com/moby/moby/client/internal/timestamp"
12 12
 )
13 13
 
14 14
 // ContainerLogs returns the logs generated by a container in an [io.ReadCloser].
... ...
@@ -53,7 +53,7 @@ func (cli *Client) ContainerLogs(ctx context.Context, containerID string, option
53 53
 	}
54 54
 
55 55
 	if options.Since != "" {
56
-		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
56
+		ts, err := timestamp.GetTimestamp(options.Since, time.Now())
57 57
 		if err != nil {
58 58
 			return nil, fmt.Errorf(`invalid value for "since": %w`, err)
59 59
 		}
... ...
@@ -61,7 +61,7 @@ func (cli *Client) ContainerLogs(ctx context.Context, containerID string, option
61 61
 	}
62 62
 
63 63
 	if options.Until != "" {
64
-		ts, err := timetypes.GetTimestamp(options.Until, time.Now())
64
+		ts, err := timestamp.GetTimestamp(options.Until, time.Now())
65 65
 		if err != nil {
66 66
 			return nil, fmt.Errorf(`invalid value for "until": %w`, err)
67 67
 		}
68 68
new file mode 100644
... ...
@@ -0,0 +1,131 @@
0
+package timestamp
1
+
2
+import (
3
+	"fmt"
4
+	"math"
5
+	"strconv"
6
+	"strings"
7
+	"time"
8
+)
9
+
10
+// These are additional predefined layouts for use in Time.Format and Time.Parse
11
+// with --since and --until parameters for `docker logs` and `docker events`
12
+const (
13
+	rFC3339Local     = "2006-01-02T15:04:05"           // RFC3339 with local timezone
14
+	rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone
15
+	dateWithZone     = "2006-01-02Z07:00"              // RFC3339 with time at 00:00:00
16
+	dateLocal        = "2006-01-02"                    // RFC3339 with local timezone and time at 00:00:00
17
+)
18
+
19
+// GetTimestamp tries to parse given string as golang duration,
20
+// then RFC3339 time and finally as a Unix timestamp. If
21
+// any of these were successful, it returns a Unix timestamp
22
+// as string otherwise returns the given value back.
23
+// In case of duration input, the returned timestamp is computed
24
+// as the given reference time minus the amount of the duration.
25
+func GetTimestamp(value string, reference time.Time) (string, error) {
26
+	if d, err := time.ParseDuration(value); value != "0" && err == nil {
27
+		return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil
28
+	}
29
+
30
+	var format string
31
+	// if the string has a Z or a + or three dashes use parse otherwise use parseinlocation
32
+	parseInLocation := !strings.ContainsAny(value, "zZ+") && strings.Count(value, "-") != 3
33
+
34
+	if strings.Contains(value, ".") {
35
+		if parseInLocation {
36
+			format = rFC3339NanoLocal
37
+		} else {
38
+			format = time.RFC3339Nano
39
+		}
40
+	} else if strings.Contains(value, "T") {
41
+		// we want the number of colons in the T portion of the timestamp
42
+		tcolons := strings.Count(value, ":")
43
+		// if parseInLocation is off and we have a +/- zone offset (not Z) then
44
+		// there will be an extra colon in the input for the tz offset subtract that
45
+		// colon from the tcolons count
46
+		if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 {
47
+			tcolons--
48
+		}
49
+		if parseInLocation {
50
+			switch tcolons {
51
+			case 0:
52
+				format = "2006-01-02T15"
53
+			case 1:
54
+				format = "2006-01-02T15:04"
55
+			default:
56
+				format = rFC3339Local
57
+			}
58
+		} else {
59
+			switch tcolons {
60
+			case 0:
61
+				format = "2006-01-02T15Z07:00"
62
+			case 1:
63
+				format = "2006-01-02T15:04Z07:00"
64
+			default:
65
+				format = time.RFC3339
66
+			}
67
+		}
68
+	} else if parseInLocation {
69
+		format = dateLocal
70
+	} else {
71
+		format = dateWithZone
72
+	}
73
+
74
+	var t time.Time
75
+	var err error
76
+
77
+	if parseInLocation {
78
+		t, err = time.ParseInLocation(format, value, time.FixedZone(reference.Zone()))
79
+	} else {
80
+		t, err = time.Parse(format, value)
81
+	}
82
+
83
+	if err != nil {
84
+		// if there is a `-` then it's an RFC3339 like timestamp
85
+		if strings.Contains(value, "-") {
86
+			return "", err // was probably an RFC3339 like timestamp but the parser failed with an error
87
+		}
88
+		if _, _, err := parseTimestamp(value); err != nil {
89
+			return "", fmt.Errorf("failed to parse value as time or duration: %q", value)
90
+		}
91
+		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)
92
+	}
93
+
94
+	return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
95
+}
96
+
97
+// ParseTimestamps returns seconds and nanoseconds from a timestamp that has
98
+// the format ("%d.%09d", time.Unix(), int64(time.Nanosecond())).
99
+// If the incoming nanosecond portion is longer than 9 digits it is truncated.
100
+// The expectation is that the seconds and nanoseconds will be used to create a
101
+// time variable.  For example:
102
+//
103
+//	seconds, nanoseconds, _ := ParseTimestamp("1136073600.000000001",0)
104
+//	since := time.Unix(seconds, nanoseconds)
105
+//
106
+// returns seconds as defaultSeconds if value == ""
107
+func ParseTimestamps(value string, defaultSeconds int64) (seconds int64, nanoseconds int64, _ error) {
108
+	if value == "" {
109
+		return defaultSeconds, 0, nil
110
+	}
111
+	return parseTimestamp(value)
112
+}
113
+
114
+func parseTimestamp(value string) (seconds int64, nanoseconds int64, _ error) {
115
+	s, n, ok := strings.Cut(value, ".")
116
+	sec, err := strconv.ParseInt(s, 10, 64)
117
+	if err != nil {
118
+		return sec, 0, err
119
+	}
120
+	if !ok {
121
+		return sec, 0, nil
122
+	}
123
+	nsec, err := strconv.ParseInt(n, 10, 64)
124
+	if err != nil {
125
+		return sec, nsec, err
126
+	}
127
+	// should already be in nanoseconds but just in case convert n to nanoseconds
128
+	nsec = int64(float64(nsec) * math.Pow(float64(10), float64(9-len(n))))
129
+	return sec, nsec, nil
130
+}
0 131
new file mode 100644
... ...
@@ -0,0 +1,95 @@
0
+package timestamp
1
+
2
+import (
3
+	"fmt"
4
+	"testing"
5
+	"time"
6
+)
7
+
8
+func TestGetTimestamp(t *testing.T) {
9
+	now := time.Now().In(time.UTC)
10
+	cases := []struct {
11
+		in, expected string
12
+		expectedErr  bool
13
+	}{
14
+		// Partial RFC3339 strings get parsed with second precision
15
+		{"2006-01-02T15:04:05.999999999+07:00", "1136189045.999999999", false},
16
+		{"2006-01-02T15:04:05.999999999Z", "1136214245.999999999", false},
17
+		{"2006-01-02T15:04:05.999999999", "1136214245.999999999", false},
18
+		{"2006-01-02T15:04:05Z", "1136214245.000000000", false},
19
+		{"2006-01-02T15:04:05", "1136214245.000000000", false},
20
+		{"2006-01-02T15:04:0Z", "", true},
21
+		{"2006-01-02T15:04:0", "", true},
22
+		{"2006-01-02T15:04Z", "1136214240.000000000", false},
23
+		{"2006-01-02T15:04+00:00", "1136214240.000000000", false},
24
+		{"2006-01-02T15:04-00:00", "1136214240.000000000", false},
25
+		{"2006-01-02T15:04", "1136214240.000000000", false},
26
+		{"2006-01-02T15:0Z", "", true},
27
+		{"2006-01-02T15:0", "", true},
28
+		{"2006-01-02T15Z", "1136214000.000000000", false},
29
+		{"2006-01-02T15+00:00", "1136214000.000000000", false},
30
+		{"2006-01-02T15-00:00", "1136214000.000000000", false},
31
+		{"2006-01-02T15", "1136214000.000000000", false},
32
+		{"2006-01-02T1Z", "1136163600.000000000", false},
33
+		{"2006-01-02T1", "1136163600.000000000", false},
34
+		{"2006-01-02TZ", "", true},
35
+		{"2006-01-02T", "", true},
36
+		{"2006-01-02+00:00", "1136160000.000000000", false},
37
+		{"2006-01-02-00:00", "1136160000.000000000", false},
38
+		{"2006-01-02-00:01", "1136160060.000000000", false},
39
+		{"2006-01-02Z", "1136160000.000000000", false},
40
+		{"2006-01-02", "1136160000.000000000", false},
41
+		{"2015-05-13T20:39:09Z", "1431549549.000000000", false},
42
+
43
+		// unix timestamps returned as is
44
+		{"1136073600", "1136073600", false},
45
+		{"1136073600.000000001", "1136073600.000000001", false},
46
+		// Durations
47
+		{"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix()), false},
48
+		{"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
49
+		{"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
50
+
51
+		{"invalid", "", true},
52
+		{"", "", true},
53
+	}
54
+
55
+	for _, c := range cases {
56
+		o, err := GetTimestamp(c.in, now)
57
+		if o != c.expected ||
58
+			(err == nil && c.expectedErr) ||
59
+			(err != nil && !c.expectedErr) {
60
+			t.Errorf("wrong value for '%s'. expected:'%s' got:'%s' with error: `%s`", c.in, c.expected, o, err)
61
+			t.Fail()
62
+		}
63
+	}
64
+}
65
+
66
+func TestParseTimestamps(t *testing.T) {
67
+	cases := []struct {
68
+		in                        string
69
+		def, expectedS, expectedN int64
70
+		expectedErr               bool
71
+	}{
72
+		// unix timestamps
73
+		{"1136073600", 0, 1136073600, 0, false},
74
+		{"1136073600.000000001", 0, 1136073600, 1, false},
75
+		{"1136073600.0000000010", 0, 1136073600, 1, false},
76
+		{"1136073600.0000000001", 0, 1136073600, 0, false},
77
+		{"1136073600.0000000009", 0, 1136073600, 0, false},
78
+		{"1136073600.00000001", 0, 1136073600, 10, false},
79
+		{"foo.bar", 0, 0, 0, true},
80
+		{"1136073600.bar", 0, 1136073600, 0, true},
81
+		{"", -1, -1, 0, false},
82
+	}
83
+
84
+	for _, c := range cases {
85
+		s, n, err := ParseTimestamps(c.in, c.def)
86
+		if s != c.expectedS ||
87
+			n != c.expectedN ||
88
+			(err == nil && c.expectedErr) ||
89
+			(err != nil && !c.expectedErr) {
90
+			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)
91
+			t.Fail()
92
+		}
93
+	}
94
+}
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"time"
9 9
 
10 10
 	"github.com/moby/moby/api/types/container"
11
-	timetypes "github.com/moby/moby/api/types/time"
11
+	"github.com/moby/moby/client/internal/timestamp"
12 12
 )
13 13
 
14 14
 // ServiceLogs returns the logs generated by a service in an [io.ReadCloser].
... ...
@@ -29,7 +29,7 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options co
29 29
 	}
30 30
 
31 31
 	if options.Since != "" {
32
-		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
32
+		ts, err := timestamp.GetTimestamp(options.Since, time.Now())
33 33
 		if err != nil {
34 34
 			return nil, fmt.Errorf(`invalid value for "since": %w`, err)
35 35
 		}
... ...
@@ -8,8 +8,8 @@ import (
8 8
 
9 9
 	"github.com/moby/moby/api/types/events"
10 10
 	"github.com/moby/moby/api/types/filters"
11
-	timetypes "github.com/moby/moby/api/types/time"
12 11
 	"github.com/moby/moby/api/types/versions"
12
+	"github.com/moby/moby/client/internal/timestamp"
13 13
 )
14 14
 
15 15
 // Events returns a stream of events in the daemon. It's up to the caller to close the stream
... ...
@@ -83,7 +83,7 @@ func buildEventsQueryParams(cliVersion string, options events.ListOptions) (url.
83 83
 	ref := time.Now()
84 84
 
85 85
 	if options.Since != "" {
86
-		ts, err := timetypes.GetTimestamp(options.Since, ref)
86
+		ts, err := timestamp.GetTimestamp(options.Since, ref)
87 87
 		if err != nil {
88 88
 			return nil, err
89 89
 		}
... ...
@@ -91,7 +91,7 @@ func buildEventsQueryParams(cliVersion string, options events.ListOptions) (url.
91 91
 	}
92 92
 
93 93
 	if options.Until != "" {
94
-		ts, err := timetypes.GetTimestamp(options.Until, ref)
94
+		ts, err := timestamp.GetTimestamp(options.Until, ref)
95 95
 		if err != nil {
96 96
 			return nil, err
97 97
 		}
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"time"
8 8
 
9 9
 	"github.com/moby/moby/api/types/container"
10
-	timetypes "github.com/moby/moby/api/types/time"
10
+	"github.com/moby/moby/client/internal/timestamp"
11 11
 )
12 12
 
13 13
 // TaskLogs returns the logs generated by a task in an [io.ReadCloser].
... ...
@@ -23,7 +23,7 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options containe
23 23
 	}
24 24
 
25 25
 	if options.Since != "" {
26
-		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
26
+		ts, err := timestamp.GetTimestamp(options.Since, time.Now())
27 27
 		if err != nil {
28 28
 			return nil, err
29 29
 		}
30 30
deleted file mode 100644
... ...
@@ -1,131 +0,0 @@
1
-package time
2
-
3
-import (
4
-	"fmt"
5
-	"math"
6
-	"strconv"
7
-	"strings"
8
-	"time"
9
-)
10
-
11
-// These are additional predefined layouts for use in Time.Format and Time.Parse
12
-// with --since and --until parameters for `docker logs` and `docker events`
13
-const (
14
-	rFC3339Local     = "2006-01-02T15:04:05"           // RFC3339 with local timezone
15
-	rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone
16
-	dateWithZone     = "2006-01-02Z07:00"              // RFC3339 with time at 00:00:00
17
-	dateLocal        = "2006-01-02"                    // RFC3339 with local timezone and time at 00:00:00
18
-)
19
-
20
-// GetTimestamp tries to parse given string as golang duration,
21
-// then RFC3339 time and finally as a Unix timestamp. If
22
-// any of these were successful, it returns a Unix timestamp
23
-// as string otherwise returns the given value back.
24
-// In case of duration input, the returned timestamp is computed
25
-// as the given reference time minus the amount of the duration.
26
-func GetTimestamp(value string, reference time.Time) (string, error) {
27
-	if d, err := time.ParseDuration(value); value != "0" && err == nil {
28
-		return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil
29
-	}
30
-
31
-	var format string
32
-	// if the string has a Z or a + or three dashes use parse otherwise use parseinlocation
33
-	parseInLocation := !strings.ContainsAny(value, "zZ+") && strings.Count(value, "-") != 3
34
-
35
-	if strings.Contains(value, ".") {
36
-		if parseInLocation {
37
-			format = rFC3339NanoLocal
38
-		} else {
39
-			format = time.RFC3339Nano
40
-		}
41
-	} else if strings.Contains(value, "T") {
42
-		// we want the number of colons in the T portion of the timestamp
43
-		tcolons := strings.Count(value, ":")
44
-		// if parseInLocation is off and we have a +/- zone offset (not Z) then
45
-		// there will be an extra colon in the input for the tz offset subtract that
46
-		// colon from the tcolons count
47
-		if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 {
48
-			tcolons--
49
-		}
50
-		if parseInLocation {
51
-			switch tcolons {
52
-			case 0:
53
-				format = "2006-01-02T15"
54
-			case 1:
55
-				format = "2006-01-02T15:04"
56
-			default:
57
-				format = rFC3339Local
58
-			}
59
-		} else {
60
-			switch tcolons {
61
-			case 0:
62
-				format = "2006-01-02T15Z07:00"
63
-			case 1:
64
-				format = "2006-01-02T15:04Z07:00"
65
-			default:
66
-				format = time.RFC3339
67
-			}
68
-		}
69
-	} else if parseInLocation {
70
-		format = dateLocal
71
-	} else {
72
-		format = dateWithZone
73
-	}
74
-
75
-	var t time.Time
76
-	var err error
77
-
78
-	if parseInLocation {
79
-		t, err = time.ParseInLocation(format, value, time.FixedZone(reference.Zone()))
80
-	} else {
81
-		t, err = time.Parse(format, value)
82
-	}
83
-
84
-	if err != nil {
85
-		// if there is a `-` then it's an RFC3339 like timestamp
86
-		if strings.Contains(value, "-") {
87
-			return "", err // was probably an RFC3339 like timestamp but the parser failed with an error
88
-		}
89
-		if _, _, err := parseTimestamp(value); err != nil {
90
-			return "", fmt.Errorf("failed to parse value as time or duration: %q", value)
91
-		}
92
-		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)
93
-	}
94
-
95
-	return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
96
-}
97
-
98
-// ParseTimestamps returns seconds and nanoseconds from a timestamp that has
99
-// the format ("%d.%09d", time.Unix(), int64(time.Nanosecond())).
100
-// If the incoming nanosecond portion is longer than 9 digits it is truncated.
101
-// The expectation is that the seconds and nanoseconds will be used to create a
102
-// time variable.  For example:
103
-//
104
-//	seconds, nanoseconds, _ := ParseTimestamp("1136073600.000000001",0)
105
-//	since := time.Unix(seconds, nanoseconds)
106
-//
107
-// returns seconds as defaultSeconds if value == ""
108
-func ParseTimestamps(value string, defaultSeconds int64) (seconds int64, nanoseconds int64, _ error) {
109
-	if value == "" {
110
-		return defaultSeconds, 0, nil
111
-	}
112
-	return parseTimestamp(value)
113
-}
114
-
115
-func parseTimestamp(value string) (seconds int64, nanoseconds int64, _ error) {
116
-	s, n, ok := strings.Cut(value, ".")
117
-	sec, err := strconv.ParseInt(s, 10, 64)
118
-	if err != nil {
119
-		return sec, 0, err
120
-	}
121
-	if !ok {
122
-		return sec, 0, nil
123
-	}
124
-	nsec, err := strconv.ParseInt(n, 10, 64)
125
-	if err != nil {
126
-		return sec, nsec, err
127
-	}
128
-	// should already be in nanoseconds but just in case convert n to nanoseconds
129
-	nsec = int64(float64(nsec) * math.Pow(float64(10), float64(9-len(n))))
130
-	return sec, nsec, nil
131
-}
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"time"
9 9
 
10 10
 	"github.com/moby/moby/api/types/container"
11
-	timetypes "github.com/moby/moby/api/types/time"
11
+	"github.com/moby/moby/client/internal/timestamp"
12 12
 )
13 13
 
14 14
 // ContainerLogs returns the logs generated by a container in an [io.ReadCloser].
... ...
@@ -53,7 +53,7 @@ func (cli *Client) ContainerLogs(ctx context.Context, containerID string, option
53 53
 	}
54 54
 
55 55
 	if options.Since != "" {
56
-		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
56
+		ts, err := timestamp.GetTimestamp(options.Since, time.Now())
57 57
 		if err != nil {
58 58
 			return nil, fmt.Errorf(`invalid value for "since": %w`, err)
59 59
 		}
... ...
@@ -61,7 +61,7 @@ func (cli *Client) ContainerLogs(ctx context.Context, containerID string, option
61 61
 	}
62 62
 
63 63
 	if options.Until != "" {
64
-		ts, err := timetypes.GetTimestamp(options.Until, time.Now())
64
+		ts, err := timestamp.GetTimestamp(options.Until, time.Now())
65 65
 		if err != nil {
66 66
 			return nil, fmt.Errorf(`invalid value for "until": %w`, err)
67 67
 		}
68 68
new file mode 100644
... ...
@@ -0,0 +1,131 @@
0
+package timestamp
1
+
2
+import (
3
+	"fmt"
4
+	"math"
5
+	"strconv"
6
+	"strings"
7
+	"time"
8
+)
9
+
10
+// These are additional predefined layouts for use in Time.Format and Time.Parse
11
+// with --since and --until parameters for `docker logs` and `docker events`
12
+const (
13
+	rFC3339Local     = "2006-01-02T15:04:05"           // RFC3339 with local timezone
14
+	rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone
15
+	dateWithZone     = "2006-01-02Z07:00"              // RFC3339 with time at 00:00:00
16
+	dateLocal        = "2006-01-02"                    // RFC3339 with local timezone and time at 00:00:00
17
+)
18
+
19
+// GetTimestamp tries to parse given string as golang duration,
20
+// then RFC3339 time and finally as a Unix timestamp. If
21
+// any of these were successful, it returns a Unix timestamp
22
+// as string otherwise returns the given value back.
23
+// In case of duration input, the returned timestamp is computed
24
+// as the given reference time minus the amount of the duration.
25
+func GetTimestamp(value string, reference time.Time) (string, error) {
26
+	if d, err := time.ParseDuration(value); value != "0" && err == nil {
27
+		return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil
28
+	}
29
+
30
+	var format string
31
+	// if the string has a Z or a + or three dashes use parse otherwise use parseinlocation
32
+	parseInLocation := !strings.ContainsAny(value, "zZ+") && strings.Count(value, "-") != 3
33
+
34
+	if strings.Contains(value, ".") {
35
+		if parseInLocation {
36
+			format = rFC3339NanoLocal
37
+		} else {
38
+			format = time.RFC3339Nano
39
+		}
40
+	} else if strings.Contains(value, "T") {
41
+		// we want the number of colons in the T portion of the timestamp
42
+		tcolons := strings.Count(value, ":")
43
+		// if parseInLocation is off and we have a +/- zone offset (not Z) then
44
+		// there will be an extra colon in the input for the tz offset subtract that
45
+		// colon from the tcolons count
46
+		if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 {
47
+			tcolons--
48
+		}
49
+		if parseInLocation {
50
+			switch tcolons {
51
+			case 0:
52
+				format = "2006-01-02T15"
53
+			case 1:
54
+				format = "2006-01-02T15:04"
55
+			default:
56
+				format = rFC3339Local
57
+			}
58
+		} else {
59
+			switch tcolons {
60
+			case 0:
61
+				format = "2006-01-02T15Z07:00"
62
+			case 1:
63
+				format = "2006-01-02T15:04Z07:00"
64
+			default:
65
+				format = time.RFC3339
66
+			}
67
+		}
68
+	} else if parseInLocation {
69
+		format = dateLocal
70
+	} else {
71
+		format = dateWithZone
72
+	}
73
+
74
+	var t time.Time
75
+	var err error
76
+
77
+	if parseInLocation {
78
+		t, err = time.ParseInLocation(format, value, time.FixedZone(reference.Zone()))
79
+	} else {
80
+		t, err = time.Parse(format, value)
81
+	}
82
+
83
+	if err != nil {
84
+		// if there is a `-` then it's an RFC3339 like timestamp
85
+		if strings.Contains(value, "-") {
86
+			return "", err // was probably an RFC3339 like timestamp but the parser failed with an error
87
+		}
88
+		if _, _, err := parseTimestamp(value); err != nil {
89
+			return "", fmt.Errorf("failed to parse value as time or duration: %q", value)
90
+		}
91
+		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)
92
+	}
93
+
94
+	return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
95
+}
96
+
97
+// ParseTimestamps returns seconds and nanoseconds from a timestamp that has
98
+// the format ("%d.%09d", time.Unix(), int64(time.Nanosecond())).
99
+// If the incoming nanosecond portion is longer than 9 digits it is truncated.
100
+// The expectation is that the seconds and nanoseconds will be used to create a
101
+// time variable.  For example:
102
+//
103
+//	seconds, nanoseconds, _ := ParseTimestamp("1136073600.000000001",0)
104
+//	since := time.Unix(seconds, nanoseconds)
105
+//
106
+// returns seconds as defaultSeconds if value == ""
107
+func ParseTimestamps(value string, defaultSeconds int64) (seconds int64, nanoseconds int64, _ error) {
108
+	if value == "" {
109
+		return defaultSeconds, 0, nil
110
+	}
111
+	return parseTimestamp(value)
112
+}
113
+
114
+func parseTimestamp(value string) (seconds int64, nanoseconds int64, _ error) {
115
+	s, n, ok := strings.Cut(value, ".")
116
+	sec, err := strconv.ParseInt(s, 10, 64)
117
+	if err != nil {
118
+		return sec, 0, err
119
+	}
120
+	if !ok {
121
+		return sec, 0, nil
122
+	}
123
+	nsec, err := strconv.ParseInt(n, 10, 64)
124
+	if err != nil {
125
+		return sec, nsec, err
126
+	}
127
+	// should already be in nanoseconds but just in case convert n to nanoseconds
128
+	nsec = int64(float64(nsec) * math.Pow(float64(10), float64(9-len(n))))
129
+	return sec, nsec, nil
130
+}
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"time"
9 9
 
10 10
 	"github.com/moby/moby/api/types/container"
11
-	timetypes "github.com/moby/moby/api/types/time"
11
+	"github.com/moby/moby/client/internal/timestamp"
12 12
 )
13 13
 
14 14
 // ServiceLogs returns the logs generated by a service in an [io.ReadCloser].
... ...
@@ -29,7 +29,7 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options co
29 29
 	}
30 30
 
31 31
 	if options.Since != "" {
32
-		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
32
+		ts, err := timestamp.GetTimestamp(options.Since, time.Now())
33 33
 		if err != nil {
34 34
 			return nil, fmt.Errorf(`invalid value for "since": %w`, err)
35 35
 		}
... ...
@@ -8,8 +8,8 @@ import (
8 8
 
9 9
 	"github.com/moby/moby/api/types/events"
10 10
 	"github.com/moby/moby/api/types/filters"
11
-	timetypes "github.com/moby/moby/api/types/time"
12 11
 	"github.com/moby/moby/api/types/versions"
12
+	"github.com/moby/moby/client/internal/timestamp"
13 13
 )
14 14
 
15 15
 // Events returns a stream of events in the daemon. It's up to the caller to close the stream
... ...
@@ -83,7 +83,7 @@ func buildEventsQueryParams(cliVersion string, options events.ListOptions) (url.
83 83
 	ref := time.Now()
84 84
 
85 85
 	if options.Since != "" {
86
-		ts, err := timetypes.GetTimestamp(options.Since, ref)
86
+		ts, err := timestamp.GetTimestamp(options.Since, ref)
87 87
 		if err != nil {
88 88
 			return nil, err
89 89
 		}
... ...
@@ -91,7 +91,7 @@ func buildEventsQueryParams(cliVersion string, options events.ListOptions) (url.
91 91
 	}
92 92
 
93 93
 	if options.Until != "" {
94
-		ts, err := timetypes.GetTimestamp(options.Until, ref)
94
+		ts, err := timestamp.GetTimestamp(options.Until, ref)
95 95
 		if err != nil {
96 96
 			return nil, err
97 97
 		}
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"time"
8 8
 
9 9
 	"github.com/moby/moby/api/types/container"
10
-	timetypes "github.com/moby/moby/api/types/time"
10
+	"github.com/moby/moby/client/internal/timestamp"
11 11
 )
12 12
 
13 13
 // TaskLogs returns the logs generated by a task in an [io.ReadCloser].
... ...
@@ -23,7 +23,7 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options containe
23 23
 	}
24 24
 
25 25
 	if options.Since != "" {
26
-		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
26
+		ts, err := timestamp.GetTimestamp(options.Since, time.Now())
27 27
 		if err != nil {
28 28
 			return nil, err
29 29
 		}
... ...
@@ -965,12 +965,12 @@ github.com/moby/moby/api/types/registry
965 965
 github.com/moby/moby/api/types/storage
966 966
 github.com/moby/moby/api/types/swarm
967 967
 github.com/moby/moby/api/types/system
968
-github.com/moby/moby/api/types/time
969 968
 github.com/moby/moby/api/types/versions
970 969
 github.com/moby/moby/api/types/volume
971 970
 # github.com/moby/moby/client v0.0.0 => ./client
972 971
 ## explicit; go 1.23.0
973 972
 github.com/moby/moby/client
973
+github.com/moby/moby/client/internal/timestamp
974 974
 github.com/moby/moby/client/pkg/jsonmessage
975 975
 github.com/moby/moby/client/pkg/stringid
976 976
 # github.com/moby/patternmatcher v0.6.0