github.com/opentofu/opentofu@v1.7.1/internal/lang/funcs/datetime.go (about)

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