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