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