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 <mholt@users.noreply.github.com>
This commit is contained in:
Francis Lavoie 2020-05-11 18:41:11 -04:00 committed by GitHub
parent 35e1d92d58
commit ef6e53bb5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 167 additions and 18 deletions

View file

@ -486,7 +486,7 @@ func Validate(cfg *Config) error {
// Duration can be an integer or a string. An integer is // Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go // interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`; // 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 type Duration time.Duration
// UnmarshalJSON satisfies json.Unmarshaler. // UnmarshalJSON satisfies json.Unmarshaler.
@ -497,7 +497,7 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
var dur time.Duration var dur time.Duration
var err error var err error
if b[0] == byte('"') && b[len(b)-1] == byte('"') { 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 { } else {
err = json.Unmarshal(b, &dur) err = json.Unmarshal(b, &dur)
} }
@ -505,6 +505,34 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
return err 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 // GoModule returns the build info of this Caddy
// build from debug.BuildInfo (requires Go modules). // build from debug.BuildInfo (requires Go modules).
// If no version information is available, a non-nil // If no version information is available, a non-nil

74
caddy_test.go Normal file
View file

@ -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)
}
}
}

View file

@ -16,7 +16,6 @@ package httpcaddyfile
import ( import (
"strconv" "strconv"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@ -227,7 +226,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
if !d.NextArg() { if !d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -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"
}
}
}
}
}
}`)
}

View file

@ -311,7 +311,7 @@ func (f Flags) Float64(name string) float64 {
// is not a duration type. It panics if the flag is // is not a duration type. It panics if the flag is
// not in the flag set. // not in the flag set.
func (f Flags) Duration(name string) time.Duration { func (f Flags) Duration(name string) time.Duration {
val, _ := time.ParseDuration(f.String(name)) val, _ := caddy.ParseDuration(f.String(name))
return val return val
} }

View file

@ -21,7 +21,6 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
@ -250,7 +249,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if h.LoadBalancing == nil { if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing) h.LoadBalancing = new(LoadBalancing)
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad duration value %s: %v", d.Val(), err) 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 { if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing) h.LoadBalancing = new(LoadBalancing)
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad interval value '%s': %v", d.Val(), err) 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 { if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks) h.HealthChecks.Active = new(ActiveHealthChecks)
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad interval value %s: %v", d.Val(), err) 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 { if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks) h.HealthChecks.Active = new(ActiveHealthChecks)
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad timeout value %s: %v", d.Val(), err) 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 { if h.HealthChecks.Passive == nil {
h.HealthChecks.Passive = new(PassiveHealthChecks) h.HealthChecks.Passive = new(PassiveHealthChecks)
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err) 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 { if h.HealthChecks.Passive == nil {
h.HealthChecks.Passive = new(PassiveHealthChecks) h.HealthChecks.Passive = new(PassiveHealthChecks)
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err) 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 { if fi, err := strconv.Atoi(d.Val()); err == nil {
h.FlushInterval = caddy.Duration(fi) h.FlushInterval = caddy.Duration(fi)
} else { } else {
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err) 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() { if !d.NextArg() {
return d.ArgErr() return d.ArgErr()
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err) 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() { if !d.NextArg() {
return d.ArgErr() return d.ArgErr()
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err) 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 h.KeepAlive.Enabled = &disable
break break
} }
dur, err := time.ParseDuration(d.Val()) dur, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err) return d.Errf("bad duration value '%s': %v", d.Val(), err)
} }

View file

@ -21,7 +21,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@ -194,7 +193,7 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.AllArgs(&keepForStr) { if !d.AllArgs(&keepForStr) {
return d.ArgErr() return d.ArgErr()
} }
keepFor, err := time.ParseDuration(keepForStr) keepFor, err := caddy.ParseDuration(keepForStr)
if err != nil { if err != nil {
return d.Errf("parsing roll_keep_for duration: %v", err) return d.Errf("parsing roll_keep_for duration: %v", err)
} }