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  }