diff --git a/util/templates/utils.go b/util/templates/funcmap.go similarity index 91% rename from util/templates/utils.go rename to util/templates/funcmap.go index 429fdfc4b..d345e7ff2 100644 --- a/util/templates/utils.go +++ b/util/templates/funcmap.go @@ -78,9 +78,11 @@ func FuncMap(tmpl *template.Template) *template.Template { } return res }, - "urlEncode": url.QueryEscape, - "unquote": unquote, - "quote": yamlQuote, + "urlEncode": url.QueryEscape, + "unquote": unquote, + "quote": yamlQuote, + "asDuration": asDuration, + "durationSeconds": durationSeconds, } return tmpl.Funcs(sprig.FuncMap()).Funcs(funcMap) diff --git a/util/templates/funcmap_duration.go b/util/templates/funcmap_duration.go new file mode 100644 index 000000000..de9cf82f2 --- /dev/null +++ b/util/templates/funcmap_duration.go @@ -0,0 +1,72 @@ +package templates + +import ( + "fmt" + "math" + "reflect" + "strconv" + "strings" + "time" +) + +// ----------------------------------------------------------------------------- +// Duration helpers (numeric-only returns) +// Source: https://github.com/Masterminds/sprig/pull/467 +// ----------------------------------------------------------------------------- + +// asDuration converts common template values into a time.Duration. +// +// Supported inputs: +// - time.Duration +// - string duration values parsed by time.ParseDuration (e.g. "1h2m3s") +// - numeric strings treated as seconds (e.g. "2.5") +// - ints and uints treated as seconds +// - floats treated as seconds +func asDuration(v any) (time.Duration, error) { + switch x := v.(type) { + case time.Duration: + return x, nil + + case string: + s := strings.TrimSpace(x) + if s == "" { + return 0, fmt.Errorf("empty duration") + } + if d, err := time.ParseDuration(s); err == nil { + return d, nil + } + if f, err := strconv.ParseFloat(s, 64); err == nil { + return time.Duration(f * float64(time.Second)), nil + } + return 0, fmt.Errorf("could not parse duration %q", x) + + case nil: + return 0, fmt.Errorf("invalid duration") + } + + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return time.Duration(rv.Int()) * time.Second, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + u := rv.Uint() + if u > uint64(math.MaxInt64) { + return 0, fmt.Errorf("duration seconds overflow: %d", u) + } + return time.Duration(int64(u)) * time.Second, nil + case reflect.Float32, reflect.Float64: + return time.Duration(rv.Float() * float64(time.Second)), nil + default: + return 0, fmt.Errorf("unsupported duration type %T", v) + } +} + +// durationSeconds converts a duration to seconds (float64). +// On error it returns 0. +func durationSeconds(v any) float64 { + d, err := asDuration(v) + if err != nil { + return 0 + } + return d.Seconds() +} diff --git a/util/templates/utils_test.go b/util/templates/funcmap_test.go similarity index 100% rename from util/templates/utils_test.go rename to util/templates/funcmap_test.go