mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-05-23 22:00:54 -04:00
This is a complement to "timestamp" and "timeadd" which allows establishing the ordering of two different timestamps while taking into account their timezone offsets, which isn't otherwise possible using the existing primitives in the Terraform language.
185 lines
6.2 KiB
Go
185 lines
6.2 KiB
Go
package funcs
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
// TimestampFunc constructs a function that returns a string representation of the current date and time.
|
|
var TimestampFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{},
|
|
Type: function.StaticReturnType(cty.String),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil
|
|
},
|
|
})
|
|
|
|
// TimeAddFunc constructs a function that adds a duration to a timestamp, returning a new timestamp.
|
|
var TimeAddFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "timestamp",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "duration",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.String),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
ts, err := parseTimestamp(args[0].AsString())
|
|
if err != nil {
|
|
return cty.UnknownVal(cty.String), err
|
|
}
|
|
duration, err := time.ParseDuration(args[1].AsString())
|
|
if err != nil {
|
|
return cty.UnknownVal(cty.String), err
|
|
}
|
|
|
|
return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil
|
|
},
|
|
})
|
|
|
|
// TimeCmpFunc is a function that compares two timestamps.
|
|
var TimeCmpFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "timestamp_a",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "timestamp_b",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.Number),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
tsA, err := parseTimestamp(args[0].AsString())
|
|
if err != nil {
|
|
return cty.UnknownVal(cty.String), function.NewArgError(0, err)
|
|
}
|
|
tsB, err := parseTimestamp(args[1].AsString())
|
|
if err != nil {
|
|
return cty.UnknownVal(cty.String), function.NewArgError(1, err)
|
|
}
|
|
|
|
switch {
|
|
case tsA.Equal(tsB):
|
|
return cty.NumberIntVal(0), nil
|
|
case tsA.Before(tsB):
|
|
return cty.NumberIntVal(-1), nil
|
|
default:
|
|
// By elimintation, tsA must be after tsB.
|
|
return cty.NumberIntVal(1), nil
|
|
}
|
|
},
|
|
})
|
|
|
|
// Timestamp returns a string representation of the current date and time.
|
|
//
|
|
// In the Terraform language, timestamps are conventionally represented as
|
|
// strings using RFC 3339 "Date and Time format" syntax, and so timestamp
|
|
// returns a string in this format.
|
|
func Timestamp() (cty.Value, error) {
|
|
return TimestampFunc.Call([]cty.Value{})
|
|
}
|
|
|
|
// TimeAdd adds a duration to a timestamp, returning a new timestamp.
|
|
//
|
|
// In the Terraform language, timestamps are conventionally represented as
|
|
// strings using RFC 3339 "Date and Time format" syntax. Timeadd requires
|
|
// the timestamp argument to be a string conforming to this syntax.
|
|
//
|
|
// `duration` is a string representation of a time difference, consisting of
|
|
// sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted
|
|
// units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first
|
|
// number may be negative to indicate a negative duration, like `"-2h5m"`.
|
|
//
|
|
// The result is a string, also in RFC 3339 format, representing the result
|
|
// of adding the given direction to the given timestamp.
|
|
func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) {
|
|
return TimeAddFunc.Call([]cty.Value{timestamp, duration})
|
|
}
|
|
|
|
// TimeCmp compares two timestamps, indicating whether they are equal or
|
|
// if one is before the other.
|
|
//
|
|
// TimeCmp considers the UTC offset of each given timestamp when making its
|
|
// decision, so for example 6:00 +0200 and 4:00 UTC are equal.
|
|
//
|
|
// In the Terraform language, timestamps are conventionally represented as
|
|
// strings using RFC 3339 "Date and Time format" syntax. TimeCmp requires
|
|
// the timestamp argument to be a string conforming to this syntax.
|
|
//
|
|
// The result is always a number between -1 and 1. -1 indicates that
|
|
// timestampA is earlier than timestampB. 1 indicates that timestampA is
|
|
// later. 0 indicates that the two timestamps represent the same instant.
|
|
func TimeCmp(timestampA, timestampB cty.Value) (cty.Value, error) {
|
|
return TimeCmpFunc.Call([]cty.Value{timestampA, timestampB})
|
|
}
|
|
|
|
func parseTimestamp(ts string) (time.Time, error) {
|
|
t, err := time.Parse(time.RFC3339, ts)
|
|
if err != nil {
|
|
switch err := err.(type) {
|
|
case *time.ParseError:
|
|
// If err is a time.ParseError then its string representation is not
|
|
// appropriate since it relies on details of Go's strange date format
|
|
// representation, which a caller of our functions is not expected
|
|
// to be familiar with.
|
|
//
|
|
// Therefore we do some light transformation to get a more suitable
|
|
// error that should make more sense to our callers. These are
|
|
// still not awesome error messages, but at least they refer to
|
|
// the timestamp portions by name rather than by Go's example
|
|
// values.
|
|
if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" {
|
|
// For some reason err.Message is populated with a ": " prefix
|
|
// by the time package.
|
|
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message)
|
|
}
|
|
var what string
|
|
switch err.LayoutElem {
|
|
case "2006":
|
|
what = "year"
|
|
case "01":
|
|
what = "month"
|
|
case "02":
|
|
what = "day of month"
|
|
case "15":
|
|
what = "hour"
|
|
case "04":
|
|
what = "minute"
|
|
case "05":
|
|
what = "second"
|
|
case "Z07:00":
|
|
what = "UTC offset"
|
|
case "T":
|
|
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'")
|
|
case ":", "-":
|
|
if err.ValueElem == "" {
|
|
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem)
|
|
} else {
|
|
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem)
|
|
}
|
|
default:
|
|
// Should never get here, because time.RFC3339 includes only the
|
|
// above portions, but since that might change in future we'll
|
|
// be robust here.
|
|
what = "timestamp segment"
|
|
}
|
|
if err.ValueElem == "" {
|
|
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what)
|
|
} else {
|
|
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what)
|
|
}
|
|
}
|
|
return time.Time{}, err
|
|
}
|
|
return t, nil
|
|
}
|