From 494c20db5fd5a5c4a98d545b0b248b0e075d298d Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 2 Nov 2020 19:26:19 +0100 Subject: [PATCH] gtime: Add ParseDuration function (#28525) * gtime: Make calculations independent of current time Signed-off-by: Arve Knudsen * Introduce gtime.ParseDuration function Signed-off-by: Arve Knudsen * gtime: Fix ParseDuration Signed-off-by: Arve Knudsen --- pkg/components/gtime/gtime.go | 67 +++++++++++++++++--- pkg/components/gtime/gtime_test.go | 56 ++++++++++++---- pkg/middleware/middleware_test.go | 10 ++- pkg/services/dashboards/dashboard_service.go | 4 +- pkg/setting/setting.go | 8 +-- pkg/setting/setting_test.go | 18 ++++-- 6 files changed, 127 insertions(+), 36 deletions(-) diff --git a/pkg/components/gtime/gtime.go b/pkg/components/gtime/gtime.go index e1e00d965c9..444e6cc388e 100644 --- a/pkg/components/gtime/gtime.go +++ b/pkg/components/gtime/gtime.go @@ -10,17 +10,19 @@ import ( var dateUnitPattern = regexp.MustCompile(`^(\d+)([dwMy])$`) // ParseInterval parses an interval with support for all units that Grafana uses. -func ParseInterval(interval string) (time.Duration, error) { - result := dateUnitPattern.FindSubmatch([]byte(interval)) - - if len(result) != 3 { - return time.ParseDuration(interval) +// An interval is relative the current wall time. +func ParseInterval(inp string) (time.Duration, error) { + dur, period, err := parse(inp) + if err != nil { + return 0, err + } + if period == "" { + return dur, nil } - num, _ := strconv.Atoi(string(result[1])) - period := string(result[2]) - now := time.Now() + num := int(dur) + now := time.Now() switch period { case "d": return now.Sub(now.AddDate(0, 0, -num)), nil @@ -32,5 +34,52 @@ func ParseInterval(interval string) (time.Duration, error) { return now.Sub(now.AddDate(-num, 0, 0)), nil } - return 0, fmt.Errorf("ParseInterval: invalid duration %q", interval) + return 0, fmt.Errorf("invalid interval %q", inp) +} + +// ParseDuration parses a duration with support for all units that Grafana uses. +// Durations are independent of wall time. +func ParseDuration(inp string) (time.Duration, error) { + dur, period, err := parse(inp) + if err != nil { + return 0, err + } + if period == "" { + return dur, nil + } + + // The average number of days in a year, using the Julian calendar + const daysInAYear = 365.25 + const day = 24 * time.Hour + const week = 7 * day + const year = time.Duration(float64(day) * daysInAYear) + const month = time.Duration(float64(year) / 12) + + switch period { + case "d": + return dur * day, nil + case "w": + return dur * week, nil + case "M": + return dur * month, nil + case "y": + return dur * year, nil + } + + return 0, fmt.Errorf("invalid duration %q", inp) +} + +func parse(inp string) (time.Duration, string, error) { + result := dateUnitPattern.FindSubmatch([]byte(inp)) + if len(result) != 3 { + dur, err := time.ParseDuration(inp) + return dur, "", err + } + + num, err := strconv.Atoi(string(result[1])) + if err != nil { + return 0, "", err + } + + return time.Duration(num), string(result[2]), nil } diff --git a/pkg/components/gtime/gtime_test.go b/pkg/components/gtime/gtime_test.go index 7ee4c6a0f80..3b21cc334ce 100644 --- a/pkg/components/gtime/gtime_test.go +++ b/pkg/components/gtime/gtime_test.go @@ -13,27 +13,57 @@ func TestParseInterval(t *testing.T) { now := time.Now() tcs := []struct { - interval string + inp string duration time.Duration err *regexp.Regexp }{ - {interval: "1d", duration: now.Sub(now.AddDate(0, 0, -1))}, - {interval: "1w", duration: now.Sub(now.AddDate(0, 0, -7))}, - {interval: "2w", duration: now.Sub(now.AddDate(0, 0, -14))}, - {interval: "1M", duration: now.Sub(now.AddDate(0, -1, 0))}, - {interval: "1y", duration: now.Sub(now.AddDate(-1, 0, 0))}, - {interval: "5y", duration: now.Sub(now.AddDate(-5, 0, 0))}, - {interval: "invalid-duration", err: regexp.MustCompile(`^time: invalid duration "?invalid-duration"?$`)}, + {inp: "1d", duration: now.Sub(now.AddDate(0, 0, -1))}, + {inp: "1w", duration: now.Sub(now.AddDate(0, 0, -7))}, + {inp: "2w", duration: now.Sub(now.AddDate(0, 0, -14))}, + {inp: "1M", duration: now.Sub(now.AddDate(0, -1, 0))}, + {inp: "1y", duration: now.Sub(now.AddDate(-1, 0, 0))}, + {inp: "5y", duration: now.Sub(now.AddDate(-5, 0, 0))}, + {inp: "invalid-duration", err: regexp.MustCompile(`^time: invalid duration "?invalid-duration"?$`)}, } - for i, tc := range tcs { t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) { - res, err := ParseInterval(tc.interval) + res, err := ParseInterval(tc.inp) if tc.err == nil { - require.NoError(t, err, "interval %q", tc.interval) - require.Equal(t, tc.duration, res, "interval %q", tc.interval) + require.NoError(t, err, "input %q", tc.inp) + require.Equal(t, tc.duration, res, "input %q", tc.inp) } else { - require.Error(t, err, "interval %q", tc.interval) + require.Error(t, err, "input %q", tc.inp) + require.Regexp(t, tc.err, err.Error()) + } + }) + } +} + +func TestParseDuration(t *testing.T) { + tcs := []struct { + inp string + duration time.Duration + err *regexp.Regexp + }{ + {inp: "1s", duration: time.Second}, + {inp: "1m", duration: time.Minute}, + {inp: "1h", duration: time.Hour}, + {inp: "1d", duration: 24 * time.Hour}, + {inp: "1w", duration: 7 * 24 * time.Hour}, + {inp: "2w", duration: 2 * 7 * 24 * time.Hour}, + {inp: "1M", duration: time.Duration(730.5 * float64(time.Hour))}, + {inp: "1y", duration: 365.25 * 24 * time.Hour}, + {inp: "5y", duration: 5 * 365.25 * 24 * time.Hour}, + {inp: "invalid-duration", err: regexp.MustCompile(`^time: invalid duration "?invalid-duration"?$`)}, + } + for i, tc := range tcs { + t.Run(fmt.Sprintf("testcase %d", i), func(t *testing.T) { + res, err := ParseDuration(tc.inp) + if tc.err == nil { + require.NoError(t, err, "input %q", tc.inp) + require.Equal(t, tc.duration, res, "input %q", tc.inp) + } else { + require.Error(t, err, "input %q", tc.inp) require.Regexp(t, tc.err, err.Error()) } }) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 7291e8222f6..73f01538714 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -546,7 +546,9 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) { defer bus.ClearBusHandlers() setting.LoginCookieName = "grafana_session" - setting.LoginMaxLifetime, _ = gtime.ParseInterval("30d") + var err error + setting.LoginMaxLifetime, err = gtime.ParseDuration("30d") + require.NoError(t, err) sc := &scenarioContext{} @@ -637,7 +639,11 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) { func initTokenRotationTest(ctx context.Context) (*models.ReqContext, *httptest.ResponseRecorder, error) { setting.LoginCookieName = "login_token" - setting.LoginMaxLifetime, _ = gtime.ParseInterval("7d") + var err error + setting.LoginMaxLifetime, err = gtime.ParseDuration("7d") + if err != nil { + return nil, nil, err + } rr := httptest.NewRecorder() req, err := http.NewRequestWithContext(ctx, "", "", nil) diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 13611b49be8..4d0559a3469 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -190,11 +190,11 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { return nil } - minRefreshInterval, err := gtime.ParseInterval(setting.MinRefreshInterval) + minRefreshInterval, err := gtime.ParseDuration(setting.MinRefreshInterval) if err != nil { return err } - d, err := gtime.ParseInterval(refresh) + d, err := gtime.ParseDuration(refresh) if err != nil { return err } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 99864a7bb47..a35ffbb8a22 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -452,7 +452,7 @@ func (cfg *Cfg) readAnnotationSettings() { alertingSection := cfg.Raw.Section("alerting") var newAnnotationCleanupSettings = func(section *ini.Section, maxAgeField string) AnnotationCleanupSettings { - maxAge, err := gtime.ParseInterval(section.Key(maxAgeField).MustString("")) + maxAge, err := gtime.ParseDuration(section.Key(maxAgeField).MustString("")) if err != nil { maxAge = 0 } @@ -1018,7 +1018,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { maxInactiveDaysVal = "7d" } maxInactiveDurationVal := valueAsString(auth, "login_maximum_inactive_lifetime_duration", maxInactiveDaysVal) - cfg.LoginMaxInactiveLifetime, err = gtime.ParseInterval(maxInactiveDurationVal) + cfg.LoginMaxInactiveLifetime, err = gtime.ParseDuration(maxInactiveDurationVal) if err != nil { return err } @@ -1031,7 +1031,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { maxLifetimeDaysVal = "7d" } maxLifetimeDurationVal := valueAsString(auth, "login_maximum_lifetime_duration", maxLifetimeDaysVal) - cfg.LoginMaxLifetime, err = gtime.ParseInterval(maxLifetimeDurationVal) + cfg.LoginMaxLifetime, err = gtime.ParseDuration(maxLifetimeDurationVal) if err != nil { return err } @@ -1121,7 +1121,7 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error { cfg.EditorsCanAdmin = users.Key("editors_can_admin").MustBool(false) userInviteMaxLifetimeVal := valueAsString(users, "user_invite_max_lifetime_duration", "24h") - userInviteMaxLifetimeDuration, err := gtime.ParseInterval(userInviteMaxLifetimeVal) + userInviteMaxLifetimeDuration, err := gtime.ParseDuration(userInviteMaxLifetimeVal) if err != nil { return err } diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 3434c46ed58..0fb696769fd 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -215,7 +215,8 @@ func TestLoadingSettings(t *testing.T) { }) So(err, ShouldBeNil) - hostname, _ := os.Hostname() + hostname, err := os.Hostname() + So(err, ShouldBeNil) So(InstanceName, ShouldEqual, hostname) }) @@ -291,7 +292,7 @@ func TestLoadingSettings(t *testing.T) { }) } -func TestParseAppUrlAndSubUrl(t *testing.T) { +func TestParseAppURLAndSubURL(t *testing.T) { testCases := []struct { rootURL string expectedAppURL string @@ -315,7 +316,10 @@ func TestParseAppUrlAndSubUrl(t *testing.T) { require.Equal(t, tc.expectedAppSubURL, appSubURL) } } + func TestAuthDurationSettings(t *testing.T) { + const maxInactiveDaysTest = 240 * time.Hour + f := ini.Empty() cfg := NewCfg() sec, err := f.NewSection("auth") @@ -324,7 +328,6 @@ func TestAuthDurationSettings(t *testing.T) { require.NoError(t, err) _, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "") require.NoError(t, err) - maxInactiveDaysTest, _ := time.ParseDuration("240h") err = readAuthSettings(f, cfg) require.NoError(t, err) require.Equal(t, maxInactiveDaysTest, cfg.LoginMaxInactiveLifetime) @@ -334,7 +337,8 @@ func TestAuthDurationSettings(t *testing.T) { require.NoError(t, err) _, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "824h") require.NoError(t, err) - maxInactiveDurationTest, _ := time.ParseDuration("824h") + maxInactiveDurationTest, err := time.ParseDuration("824h") + require.NoError(t, err) err = readAuthSettings(f, cfg) require.NoError(t, err) require.Equal(t, maxInactiveDurationTest, cfg.LoginMaxInactiveLifetime) @@ -346,7 +350,8 @@ func TestAuthDurationSettings(t *testing.T) { require.NoError(t, err) _, err = sec.NewKey("login_maximum_lifetime_duration", "") require.NoError(t, err) - maxLifetimeDaysTest, _ := time.ParseDuration("576h") + maxLifetimeDaysTest, err := time.ParseDuration("576h") + require.NoError(t, err) err = readAuthSettings(f, cfg) require.NoError(t, err) require.Equal(t, maxLifetimeDaysTest, cfg.LoginMaxLifetime) @@ -356,7 +361,8 @@ func TestAuthDurationSettings(t *testing.T) { require.NoError(t, err) _, err = sec.NewKey("login_maximum_lifetime_duration", "824h") require.NoError(t, err) - maxLifetimeDurationTest, _ := time.ParseDuration("824h") + maxLifetimeDurationTest, err := time.ParseDuration("824h") + require.NoError(t, err) err = readAuthSettings(f, cfg) require.NoError(t, err) require.Equal(t, maxLifetimeDurationTest, cfg.LoginMaxLifetime)