Browse code

Improve GetTimestamp parsing

`GetTimestamp()` "assumed" values it could not parse
to be a valid unix timestamp, and would use invalid
values ("hello world") as-is (even testing that
it did so).

This patch validates unix timestamp to be a valid
numeric value, and makes other values invalid.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2017/11/04 01:39:53
Showing 6 changed files
... ...
@@ -82,11 +82,14 @@ func GetTimestamp(value string, reference time.Time) (string, error) {
82 82
 	}
83 83
 
84 84
 	if err != nil {
85
-		// if there is a `-` then it's an RFC3339 like timestamp otherwise assume unixtimestamp
85
+		// if there is a `-` then it's an RFC3339 like timestamp
86 86
 		if strings.Contains(value, "-") {
87 87
 			return "", err // was probably an RFC3339 like timestamp but the parser failed with an error
88 88
 		}
89
-		return value, nil // unixtimestamp in and out case (meaning: the value passed at the command line is already in the right format for passing to the server)
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)
90 93
 	}
91 94
 
92 95
 	return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
... ...
@@ -104,6 +107,10 @@ func ParseTimestamps(value string, def int64) (int64, int64, error) {
104 104
 	if value == "" {
105 105
 		return def, 0, nil
106 106
 	}
107
+	return parseTimestamp(value)
108
+}
109
+
110
+func parseTimestamp(value string) (int64, int64, error) {
107 111
 	sa := strings.SplitN(value, ".", 2)
108 112
 	s, err := strconv.ParseInt(sa[0], 10, 64)
109 113
 	if err != nil {
... ...
@@ -49,8 +49,8 @@ func TestGetTimestamp(t *testing.T) {
49 49
 		{"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
50 50
 		{"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
51 51
 
52
-		// String fallback
53
-		{"invalid", "invalid", false},
52
+		{"invalid", "", true},
53
+		{"", "", true},
54 54
 	}
55 55
 
56 56
 	for _, c := range cases {
... ...
@@ -8,6 +8,7 @@ import (
8 8
 
9 9
 	"github.com/docker/docker/api/types"
10 10
 	timetypes "github.com/docker/docker/api/types/time"
11
+	"github.com/pkg/errors"
11 12
 )
12 13
 
13 14
 // ContainerLogs returns the logs generated by a container in an io.ReadCloser.
... ...
@@ -45,7 +46,7 @@ func (cli *Client) ContainerLogs(ctx context.Context, container string, options
45 45
 	if options.Since != "" {
46 46
 		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
47 47
 		if err != nil {
48
-			return nil, err
48
+			return nil, errors.Wrap(err, `invalid value for "since"`)
49 49
 		}
50 50
 		query.Set("since", ts)
51 51
 	}
... ...
@@ -53,7 +54,7 @@ func (cli *Client) ContainerLogs(ctx context.Context, container string, options
53 53
 	if options.Until != "" {
54 54
 		ts, err := timetypes.GetTimestamp(options.Until, time.Now())
55 55
 		if err != nil {
56
-			return nil, err
56
+			return nil, errors.Wrap(err, `invalid value for "until"`)
57 57
 		}
58 58
 		query.Set("until", ts)
59 59
 	}
... ...
@@ -2,6 +2,7 @@ package client // import "github.com/docker/docker/client"
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"context"
5 6
 	"fmt"
6 7
 	"io"
7 8
 	"io/ioutil"
... ...
@@ -12,10 +13,9 @@ import (
12 12
 	"testing"
13 13
 	"time"
14 14
 
15
-	"context"
16
-
17 15
 	"github.com/docker/docker/api/types"
18
-	"github.com/docker/docker/internal/testutil"
16
+	"github.com/gotestyourself/gotestyourself/assert"
17
+	is "github.com/gotestyourself/gotestyourself/assert/cmp"
19 18
 )
20 19
 
21 20
 func TestContainerLogsNotFoundError(t *testing.T) {
... ...
@@ -33,17 +33,15 @@ func TestContainerLogsError(t *testing.T) {
33 33
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
34 34
 	}
35 35
 	_, err := client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{})
36
-	if err == nil || err.Error() != "Error response from daemon: Server error" {
37
-		t.Fatalf("expected a Server Error, got %v", err)
38
-	}
36
+	assert.Check(t, is.Error(err, "Error response from daemon: Server error"))
39 37
 	_, err = client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{
40 38
 		Since: "2006-01-02TZ",
41 39
 	})
42
-	testutil.ErrorContains(t, err, `parsing time "2006-01-02TZ"`)
40
+	assert.Check(t, is.ErrorContains(err, `parsing time "2006-01-02TZ"`))
43 41
 	_, err = client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{
44 42
 		Until: "2006-01-02TZ",
45 43
 	})
46
-	testutil.ErrorContains(t, err, `parsing time "2006-01-02TZ"`)
44
+	assert.Check(t, is.ErrorContains(err, `parsing time "2006-01-02TZ"`))
47 45
 }
48 46
 
49 47
 func TestContainerLogs(t *testing.T) {
... ...
@@ -51,6 +49,7 @@ func TestContainerLogs(t *testing.T) {
51 51
 	cases := []struct {
52 52
 		options             types.ContainerLogsOptions
53 53
 		expectedQueryParams map[string]string
54
+		expectedError       string
54 55
 	}{
55 56
 		{
56 57
 			expectedQueryParams: map[string]string{
... ...
@@ -84,32 +83,44 @@ func TestContainerLogs(t *testing.T) {
84 84
 		},
85 85
 		{
86 86
 			options: types.ContainerLogsOptions{
87
-				// An complete invalid date, timestamp or go duration will be
88
-				// passed as is
89
-				Since: "invalid but valid",
87
+				// timestamp will be passed as is
88
+				Since: "1136073600.000000001",
90 89
 			},
91 90
 			expectedQueryParams: map[string]string{
92 91
 				"tail":  "",
93
-				"since": "invalid but valid",
92
+				"since": "1136073600.000000001",
94 93
 			},
95 94
 		},
96 95
 		{
97 96
 			options: types.ContainerLogsOptions{
98
-				// An complete invalid date, timestamp or go duration will be
99
-				// passed as is
100
-				Until: "invalid but valid",
97
+				// timestamp will be passed as is
98
+				Until: "1136073600.000000001",
101 99
 			},
102 100
 			expectedQueryParams: map[string]string{
103 101
 				"tail":  "",
104
-				"until": "invalid but valid",
102
+				"until": "1136073600.000000001",
103
+			},
104
+		},
105
+		{
106
+			options: types.ContainerLogsOptions{
107
+				// An complete invalid date will not be passed
108
+				Since: "invalid value",
105 109
 			},
110
+			expectedError: `invalid value for "since": failed to parse value as time or duration: "invalid value"`,
111
+		},
112
+		{
113
+			options: types.ContainerLogsOptions{
114
+				// An complete invalid date will not be passed
115
+				Until: "invalid value",
116
+			},
117
+			expectedError: `invalid value for "until": failed to parse value as time or duration: "invalid value"`,
106 118
 		},
107 119
 	}
108 120
 	for _, logCase := range cases {
109 121
 		client := &Client{
110 122
 			client: newMockClient(func(r *http.Request) (*http.Response, error) {
111 123
 				if !strings.HasPrefix(r.URL.Path, expectedURL) {
112
-					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
124
+					return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, r.URL)
113 125
 				}
114 126
 				// Check query parameters
115 127
 				query := r.URL.Query()
... ...
@@ -126,17 +137,15 @@ func TestContainerLogs(t *testing.T) {
126 126
 			}),
127 127
 		}
128 128
 		body, err := client.ContainerLogs(context.Background(), "container_id", logCase.options)
129
-		if err != nil {
130
-			t.Fatal(err)
129
+		if logCase.expectedError != "" {
130
+			assert.Check(t, is.Error(err, logCase.expectedError))
131
+			continue
131 132
 		}
133
+		assert.NilError(t, err)
132 134
 		defer body.Close()
133 135
 		content, err := ioutil.ReadAll(body)
134
-		if err != nil {
135
-			t.Fatal(err)
136
-		}
137
-		if string(content) != "response" {
138
-			t.Fatalf("expected response to contain 'response', got %s", string(content))
139
-		}
136
+		assert.NilError(t, err)
137
+		assert.Check(t, is.Contains(string(content), "response"))
140 138
 	}
141 139
 }
142 140
 
... ...
@@ -8,6 +8,7 @@ import (
8 8
 
9 9
 	"github.com/docker/docker/api/types"
10 10
 	timetypes "github.com/docker/docker/api/types/time"
11
+	"github.com/pkg/errors"
11 12
 )
12 13
 
13 14
 // ServiceLogs returns the logs generated by a service in an io.ReadCloser.
... ...
@@ -25,7 +26,7 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options ty
25 25
 	if options.Since != "" {
26 26
 		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
27 27
 		if err != nil {
28
-			return nil, err
28
+			return nil, errors.Wrap(err, `invalid value for "since"`)
29 29
 		}
30 30
 		query.Set("since", ts)
31 31
 	}
... ...
@@ -2,6 +2,7 @@ package client // import "github.com/docker/docker/client"
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"context"
5 6
 	"fmt"
6 7
 	"io"
7 8
 	"io/ioutil"
... ...
@@ -12,9 +13,9 @@ import (
12 12
 	"testing"
13 13
 	"time"
14 14
 
15
-	"context"
16
-
17 15
 	"github.com/docker/docker/api/types"
16
+	"github.com/gotestyourself/gotestyourself/assert"
17
+	is "github.com/gotestyourself/gotestyourself/assert/cmp"
18 18
 )
19 19
 
20 20
 func TestServiceLogsError(t *testing.T) {
... ...
@@ -22,15 +23,11 @@ func TestServiceLogsError(t *testing.T) {
22 22
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
23 23
 	}
24 24
 	_, err := client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{})
25
-	if err == nil || err.Error() != "Error response from daemon: Server error" {
26
-		t.Fatalf("expected a Server Error, got %v", err)
27
-	}
25
+	assert.Check(t, is.Error(err, "Error response from daemon: Server error"))
28 26
 	_, err = client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{
29 27
 		Since: "2006-01-02TZ",
30 28
 	})
31
-	if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) {
32
-		t.Fatalf("expected a 'parsing time' error, got %v", err)
33
-	}
29
+	assert.Check(t, is.ErrorContains(err, `parsing time "2006-01-02TZ"`))
34 30
 }
35 31
 
36 32
 func TestServiceLogs(t *testing.T) {
... ...
@@ -38,6 +35,7 @@ func TestServiceLogs(t *testing.T) {
38 38
 	cases := []struct {
39 39
 		options             types.ContainerLogsOptions
40 40
 		expectedQueryParams map[string]string
41
+		expectedError       string
41 42
 	}{
42 43
 		{
43 44
 			expectedQueryParams: map[string]string{
... ...
@@ -71,21 +69,27 @@ func TestServiceLogs(t *testing.T) {
71 71
 		},
72 72
 		{
73 73
 			options: types.ContainerLogsOptions{
74
-				// An complete invalid date, timestamp or go duration will be
75
-				// passed as is
76
-				Since: "invalid but valid",
74
+				// timestamp will be passed as is
75
+				Since: "1136073600.000000001",
77 76
 			},
78 77
 			expectedQueryParams: map[string]string{
79 78
 				"tail":  "",
80
-				"since": "invalid but valid",
79
+				"since": "1136073600.000000001",
81 80
 			},
82 81
 		},
82
+		{
83
+			options: types.ContainerLogsOptions{
84
+				// An complete invalid date will not be passed
85
+				Since: "invalid value",
86
+			},
87
+			expectedError: `invalid value for "since": failed to parse value as time or duration: "invalid value"`,
88
+		},
83 89
 	}
84 90
 	for _, logCase := range cases {
85 91
 		client := &Client{
86 92
 			client: newMockClient(func(r *http.Request) (*http.Response, error) {
87 93
 				if !strings.HasPrefix(r.URL.Path, expectedURL) {
88
-					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
94
+					return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, r.URL)
89 95
 				}
90 96
 				// Check query parameters
91 97
 				query := r.URL.Query()
... ...
@@ -102,17 +106,15 @@ func TestServiceLogs(t *testing.T) {
102 102
 			}),
103 103
 		}
104 104
 		body, err := client.ServiceLogs(context.Background(), "service_id", logCase.options)
105
-		if err != nil {
106
-			t.Fatal(err)
105
+		if logCase.expectedError != "" {
106
+			assert.Check(t, is.Error(err, logCase.expectedError))
107
+			continue
107 108
 		}
109
+		assert.NilError(t, err)
108 110
 		defer body.Close()
109 111
 		content, err := ioutil.ReadAll(body)
110
-		if err != nil {
111
-			t.Fatal(err)
112
-		}
113
-		if string(content) != "response" {
114
-			t.Fatalf("expected response to contain 'response', got %s", string(content))
115
-		}
112
+		assert.NilError(t, err)
113
+		assert.Check(t, is.Contains(string(content), "response"))
116 114
 	}
117 115
 }
118 116