github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/funcs/datetime.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package funcs 5 6 import ( 7 "fmt" 8 "time" 9 10 "github.com/zclconf/go-cty/cty" 11 "github.com/zclconf/go-cty/cty/function" 12 ) 13 14 // TimestampFunc constructs a function that returns a string representation of the current date and time. 15 var TimestampFunc = function.New(&function.Spec{ 16 Params: []function.Parameter{}, 17 Type: function.StaticReturnType(cty.String), 18 RefineResult: refineNotNull, 19 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 20 return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil 21 }, 22 }) 23 24 // MakeStaticTimestampFunc constructs a function that returns a string 25 // representation of the date and time specified by the provided argument. 26 func MakeStaticTimestampFunc(static time.Time) function.Function { 27 return function.New(&function.Spec{ 28 Params: []function.Parameter{}, 29 Type: function.StaticReturnType(cty.String), 30 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 31 return cty.StringVal(static.Format(time.RFC3339)), nil 32 }, 33 }) 34 } 35 36 // TimeAddFunc constructs a function that adds a duration to a timestamp, returning a new timestamp. 37 var TimeAddFunc = function.New(&function.Spec{ 38 Params: []function.Parameter{ 39 { 40 Name: "timestamp", 41 Type: cty.String, 42 }, 43 { 44 Name: "duration", 45 Type: cty.String, 46 }, 47 }, 48 Type: function.StaticReturnType(cty.String), 49 RefineResult: refineNotNull, 50 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 51 ts, err := parseTimestamp(args[0].AsString()) 52 if err != nil { 53 return cty.UnknownVal(cty.String), err 54 } 55 duration, err := time.ParseDuration(args[1].AsString()) 56 if err != nil { 57 return cty.UnknownVal(cty.String), err 58 } 59 60 return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil 61 }, 62 }) 63 64 // TimeCmpFunc is a function that compares two timestamps. 65 var TimeCmpFunc = function.New(&function.Spec{ 66 Params: []function.Parameter{ 67 { 68 Name: "timestamp_a", 69 Type: cty.String, 70 }, 71 { 72 Name: "timestamp_b", 73 Type: cty.String, 74 }, 75 }, 76 Type: function.StaticReturnType(cty.Number), 77 RefineResult: refineNotNull, 78 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 79 tsA, err := parseTimestamp(args[0].AsString()) 80 if err != nil { 81 return cty.UnknownVal(cty.String), function.NewArgError(0, err) 82 } 83 tsB, err := parseTimestamp(args[1].AsString()) 84 if err != nil { 85 return cty.UnknownVal(cty.String), function.NewArgError(1, err) 86 } 87 88 switch { 89 case tsA.Equal(tsB): 90 return cty.NumberIntVal(0), nil 91 case tsA.Before(tsB): 92 return cty.NumberIntVal(-1), nil 93 default: 94 // By elimintation, tsA must be after tsB. 95 return cty.NumberIntVal(1), nil 96 } 97 }, 98 }) 99 100 // Timestamp returns a string representation of the current date and time. 101 // 102 // In the Terraform language, timestamps are conventionally represented as 103 // strings using RFC 3339 "Date and Time format" syntax, and so timestamp 104 // returns a string in this format. 105 func Timestamp() (cty.Value, error) { 106 return TimestampFunc.Call([]cty.Value{}) 107 } 108 109 // TimeAdd adds a duration to a timestamp, returning a new timestamp. 110 // 111 // In the Terraform language, timestamps are conventionally represented as 112 // strings using RFC 3339 "Date and Time format" syntax. Timeadd requires 113 // the timestamp argument to be a string conforming to this syntax. 114 // 115 // `duration` is a string representation of a time difference, consisting of 116 // sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted 117 // units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first 118 // number may be negative to indicate a negative duration, like `"-2h5m"`. 119 // 120 // The result is a string, also in RFC 3339 format, representing the result 121 // of adding the given direction to the given timestamp. 122 func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) { 123 return TimeAddFunc.Call([]cty.Value{timestamp, duration}) 124 } 125 126 // TimeCmp compares two timestamps, indicating whether they are equal or 127 // if one is before the other. 128 // 129 // TimeCmp considers the UTC offset of each given timestamp when making its 130 // decision, so for example 6:00 +0200 and 4:00 UTC are equal. 131 // 132 // In the Terraform language, timestamps are conventionally represented as 133 // strings using RFC 3339 "Date and Time format" syntax. TimeCmp requires 134 // the timestamp argument to be a string conforming to this syntax. 135 // 136 // The result is always a number between -1 and 1. -1 indicates that 137 // timestampA is earlier than timestampB. 1 indicates that timestampA is 138 // later. 0 indicates that the two timestamps represent the same instant. 139 func TimeCmp(timestampA, timestampB cty.Value) (cty.Value, error) { 140 return TimeCmpFunc.Call([]cty.Value{timestampA, timestampB}) 141 } 142 143 func parseTimestamp(ts string) (time.Time, error) { 144 t, err := time.Parse(time.RFC3339, ts) 145 if err != nil { 146 switch err := err.(type) { 147 case *time.ParseError: 148 // If err is a time.ParseError then its string representation is not 149 // appropriate since it relies on details of Go's strange date format 150 // representation, which a caller of our functions is not expected 151 // to be familiar with. 152 // 153 // Therefore we do some light transformation to get a more suitable 154 // error that should make more sense to our callers. These are 155 // still not awesome error messages, but at least they refer to 156 // the timestamp portions by name rather than by Go's example 157 // values. 158 if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" { 159 // For some reason err.Message is populated with a ": " prefix 160 // by the time package. 161 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message) 162 } 163 var what string 164 switch err.LayoutElem { 165 case "2006": 166 what = "year" 167 case "01": 168 what = "month" 169 case "02": 170 what = "day of month" 171 case "15": 172 what = "hour" 173 case "04": 174 what = "minute" 175 case "05": 176 what = "second" 177 case "Z07:00": 178 what = "UTC offset" 179 case "T": 180 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'") 181 case ":", "-": 182 if err.ValueElem == "" { 183 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem) 184 } else { 185 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem) 186 } 187 default: 188 // Should never get here, because time.RFC3339 includes only the 189 // above portions, but since that might change in future we'll 190 // be robust here. 191 what = "timestamp segment" 192 } 193 if err.ValueElem == "" { 194 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what) 195 } else { 196 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what) 197 } 198 } 199 return time.Time{}, err 200 } 201 return t, nil 202 }