From ef6e53bb5f521e4d400849b79bc72e89fe2a7484 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 11 May 2020 18:41:11 -0400 Subject: [PATCH] core: Add support for `d` duration unit (#3323) * caddy: Add support for `d` duration unit * Improvements to ParseDuration; add unit tests Co-authored-by: Matthew Holt --- caddy.go | 32 +++++++- caddy_test.go | 74 +++++++++++++++++++ caddyconfig/httpcaddyfile/options.go | 3 +- caddytest/integration/caddyfile_adapt_test.go | 50 +++++++++++++ cmd/main.go | 2 +- modules/caddyhttp/reverseproxy/caddyfile.go | 21 +++--- modules/logging/filewriter.go | 3 +- 7 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 caddy_test.go diff --git a/caddy.go b/caddy.go index 00a56e74..1184bc97 100644 --- a/caddy.go +++ b/caddy.go @@ -486,7 +486,7 @@ func Validate(cfg *Config) error { // Duration can be an integer or a string. An integer is // interpreted as nanoseconds. If a string, it is a Go // time.Duration value such as `300ms`, `1.5h`, or `2h45m`; -// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, and `h`. +// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, `h`, and `d`. type Duration time.Duration // UnmarshalJSON satisfies json.Unmarshaler. @@ -497,7 +497,7 @@ func (d *Duration) UnmarshalJSON(b []byte) error { var dur time.Duration var err error if b[0] == byte('"') && b[len(b)-1] == byte('"') { - dur, err = time.ParseDuration(strings.Trim(string(b), `"`)) + dur, err = ParseDuration(strings.Trim(string(b), `"`)) } else { err = json.Unmarshal(b, &dur) } @@ -505,6 +505,34 @@ func (d *Duration) UnmarshalJSON(b []byte) error { return err } +// ParseDuration parses a duration string, adding +// support for the "d" unit meaning number of days, +// where a day is assumed to be 24h. +func ParseDuration(s string) (time.Duration, error) { + var inNumber bool + var numStart int + for i := 0; i < len(s); i++ { + ch := s[i] + if ch == 'd' { + daysStr := s[numStart:i] + days, err := strconv.ParseFloat(daysStr, 64) + if err != nil { + return 0, err + } + hours := days * 24.0 + hoursStr := strconv.FormatFloat(hours, 'f', -1, 64) + s = s[:numStart] + hoursStr + "h" + s[i+1:] + i-- + continue + } + if !inNumber { + numStart = i + } + inNumber = (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == '+' + } + return time.ParseDuration(s) +} + // GoModule returns the build info of this Caddy // build from debug.BuildInfo (requires Go modules). // If no version information is available, a non-nil diff --git a/caddy_test.go b/caddy_test.go new file mode 100644 index 00000000..adf14350 --- /dev/null +++ b/caddy_test.go @@ -0,0 +1,74 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddy + +import ( + "testing" + "time" +) + +func TestParseDuration(t *testing.T) { + const day = 24 * time.Hour + for i, tc := range []struct { + input string + expect time.Duration + }{ + { + input: "3h", + expect: 3 * time.Hour, + }, + { + input: "1d", + expect: day, + }, + { + input: "1d30m", + expect: day + 30*time.Minute, + }, + { + input: "1m2d", + expect: time.Minute + day*2, + }, + { + input: "1m2d30s", + expect: time.Minute + day*2 + 30*time.Second, + }, + { + input: "1d2d", + expect: 3 * day, + }, + { + input: "1.5d", + expect: time.Duration(1.5 * float64(day)), + }, + { + input: "4m1.25d", + expect: 4*time.Minute + time.Duration(1.25*float64(day)), + }, + { + input: "-1.25d12h", + expect: time.Duration(-1.25*float64(day)) - 12*time.Hour, + }, + } { + actual, err := ParseDuration(tc.input) + if err != nil { + t.Errorf("Test %d ('%s'): Got error: %v", i, tc.input, err) + continue + } + if actual != tc.expect { + t.Errorf("Test %d ('%s'): Expected=%s Actual=%s", i, tc.input, tc.expect, actual) + } + } +} diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index de288db9..49a11f6e 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -16,7 +16,6 @@ package httpcaddyfile import ( "strconv" - "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -227,7 +226,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) { if !d.NextArg() { return nil, d.ArgErr() } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return nil, err } diff --git a/caddytest/integration/caddyfile_adapt_test.go b/caddytest/integration/caddyfile_adapt_test.go index c2ad892d..98c81da3 100644 --- a/caddytest/integration/caddyfile_adapt_test.go +++ b/caddytest/integration/caddyfile_adapt_test.go @@ -489,3 +489,53 @@ func TestGlobalOptions(t *testing.T) { } }`) } + +func TestLogRollDays(t *testing.T) { + caddytest.AssertAdapt(t, ` + :80 + + log { + output file /var/log/access.log { + roll_size 1gb + roll_keep 5 + roll_keep_for 90d + } + } + `, "caddyfile", `{ + "logging": { + "logs": { + "default": { + "exclude": [ + "http.log.access.log0" + ] + }, + "log0": { + "writer": { + "filename": "/var/log/access.log", + "output": "file", + "roll_keep": 5, + "roll_keep_days": 90, + "roll_size_mb": 954 + }, + "include": [ + "http.log.access.log0" + ] + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "logs": { + "default_logger_name": "log0" + } + } + } + } + } +}`) +} diff --git a/cmd/main.go b/cmd/main.go index bdc95a45..fd82b969 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -311,7 +311,7 @@ func (f Flags) Float64(name string) float64 { // is not a duration type. It panics if the flag is // not in the flag set. func (f Flags) Duration(name string) time.Duration { - val, _ := time.ParseDuration(f.String(name)) + val, _ := caddy.ParseDuration(f.String(name)) return val } diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 0a14f098..491b0674 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -21,7 +21,6 @@ import ( "reflect" "strconv" "strings" - "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -250,7 +249,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value %s: %v", d.Val(), err) } @@ -263,7 +262,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.LoadBalancing == nil { h.LoadBalancing = new(LoadBalancing) } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad interval value '%s': %v", d.Val(), err) } @@ -307,7 +306,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad interval value %s: %v", d.Val(), err) } @@ -323,7 +322,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.HealthChecks.Active == nil { h.HealthChecks.Active = new(ActiveHealthChecks) } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value %s: %v", d.Val(), err) } @@ -387,7 +386,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.HealthChecks.Passive == nil { h.HealthChecks.Passive = new(PassiveHealthChecks) } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } @@ -441,7 +440,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if h.HealthChecks.Passive == nil { h.HealthChecks.Passive = new(PassiveHealthChecks) } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } @@ -454,7 +453,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if fi, err := strconv.Atoi(d.Val()); err == nil { h.FlushInterval = caddy.Duration(fi) } else { - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } @@ -606,7 +605,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.NextArg() { return d.ArgErr() } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } @@ -641,7 +640,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.NextArg() { return d.ArgErr() } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad timeout value '%s': %v", d.Val(), err) } @@ -683,7 +682,7 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { h.KeepAlive.Enabled = &disable break } - dur, err := time.ParseDuration(d.Val()) + dur, err := caddy.ParseDuration(d.Val()) if err != nil { return d.Errf("bad duration value '%s': %v", d.Val(), err) } diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index 59f5b2ae..376deeb2 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -21,7 +21,6 @@ import ( "os" "path/filepath" "strconv" - "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -194,7 +193,7 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.AllArgs(&keepForStr) { return d.ArgErr() } - keepFor, err := time.ParseDuration(keepForStr) + keepFor, err := caddy.ParseDuration(keepForStr) if err != nil { return d.Errf("parsing roll_keep_for duration: %v", err) }