From 41dc78711eb7eadc20a2864b816c30bff9b79878 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Tue, 28 Nov 2023 13:01:10 -0500 Subject: [PATCH] hcl2template/functions: Add Non-null refinements for various functions cty's new "refinements" concept allows us to reduce the range of unknown values from our functions. This initial changeset focuses only on declaring which functions are guaranteed to return a non-null result, which is a helpful baseline refinement because it allows "== null" and "!= null" tests to produce known results even when the given value is otherwise unknown. This commit also includes some updates to test results that are now refined based on cty's own built-in refinement behaviors, just as a result of us having updated cty in the previous commit. --- hcl2template/function/datetime.go | 35 ++++++++++++++++------------ hcl2template/function/env.go | 3 ++- hcl2template/function/index.go | 3 ++- hcl2template/function/length.go | 1 + hcl2template/function/length_test.go | 18 ++++++++------ hcl2template/function/refinements.go | 9 +++++++ 6 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 hcl2template/function/refinements.go diff --git a/hcl2template/function/datetime.go b/hcl2template/function/datetime.go index 066db18db39..a2b0cc9d17c 100644 --- a/hcl2template/function/datetime.go +++ b/hcl2template/function/datetime.go @@ -12,19 +12,20 @@ import ( "github.com/zclconf/go-cty/cty/function" ) -// InitTime is the UTC time when this package was initialized. It is +// initTime is the UTC time when this package was initialized. It is // used as the timestamp for all configuration templates so that they // match for a single build. -var InitTime time.Time +var initTime time.Time func init() { - InitTime = time.Now().UTC() + initTime = time.Now().UTC() } // 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), + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil }, @@ -40,42 +41,46 @@ func Timestamp() (cty.Value, error) { } // LegacyIsotimeFunc constructs a function that returns a string representation -// of the current date and time using golang's datetime formatting. +// of the current date and time using the Go language datetime formatting syntax. var LegacyIsotimeFunc = function.New(&function.Spec{ Params: []function.Parameter{}, VarParam: &function.Parameter{ Name: "format", Type: cty.String, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { if len(args) > 1 { return cty.StringVal(""), fmt.Errorf("too many values, 1 needed: %v", args) - } else if len(args) == 0 { - return cty.StringVal(InitTime.Format(time.RFC3339)), nil + } + if len(args) == 0 { + return cty.StringVal(initTime.Format(time.RFC3339)), nil } format := args[0].AsString() - return cty.StringVal(InitTime.Format(format)), nil + return cty.StringVal(initTime.Format(format)), nil }, }) // LegacyStrftimeFunc constructs a function that returns a string representation -// of the current date and time using golang's strftime datetime formatting. +// of the current date and time using the Go language strftime datetime formatting syntax. var LegacyStrftimeFunc = function.New(&function.Spec{ Params: []function.Parameter{}, VarParam: &function.Parameter{ Name: "format", Type: cty.String, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { if len(args) > 1 { return cty.StringVal(""), fmt.Errorf("too many values, 1 needed: %v", args) - } else if len(args) == 0 { - return cty.StringVal(InitTime.Format(time.RFC3339)), nil + } + if len(args) == 0 { + return cty.StringVal(initTime.Format(time.RFC3339)), nil } format := args[0].AsString() - return cty.StringVal(strftime.Format(format, InitTime)), nil + return cty.StringVal(strftime.Format(format, initTime)), nil }, }) diff --git a/hcl2template/function/env.go b/hcl2template/function/env.go index 34c09a8b921..b6c36e68494 100644 --- a/hcl2template/function/env.go +++ b/hcl2template/function/env.go @@ -21,7 +21,8 @@ var EnvFunc = function.New(&function.Spec{ AllowUnknown: false, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { key := args[0].AsString() value := os.Getenv(key) diff --git a/hcl2template/function/index.go b/hcl2template/function/index.go index cb4f0c51921..8aade576ded 100644 --- a/hcl2template/function/index.go +++ b/hcl2template/function/index.go @@ -23,7 +23,8 @@ var IndexFunc = function.New(&function.Spec{ Type: cty.DynamicPseudoType, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) { return cty.NilVal, errors.New("argument must be a list or tuple") diff --git a/hcl2template/function/length.go b/hcl2template/function/length.go index 23b5969af10..f2b83dad925 100644 --- a/hcl2template/function/length.go +++ b/hcl2template/function/length.go @@ -29,6 +29,7 @@ var LengthFunc = function.New(&function.Spec{ return cty.Number, errors.New("argument must be a string, a collection type, or a structural type") } }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { coll := args[0] collTy := args[0].Type() diff --git a/hcl2template/function/length_test.go b/hcl2template/function/length_test.go index 0a68c126c51..9bae3886f74 100644 --- a/hcl2template/function/length_test.go +++ b/hcl2template/function/length_test.go @@ -5,6 +5,7 @@ package function import ( "fmt" + "math" "testing" "github.com/zclconf/go-cty/cty" @@ -69,11 +70,15 @@ func TestLength(t *testing.T) { }, { cty.UnknownVal(cty.List(cty.Bool)), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).Refine(). + NotNull(). + NumberRangeLowerBound(cty.Zero, true). + NumberRangeUpperBound(cty.NumberIntVal(math.MaxInt), true). + NewValue(), }, { cty.DynamicVal, - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), }, { cty.StringVal("hello"), @@ -118,11 +123,10 @@ func TestLength(t *testing.T) { }, { cty.UnknownVal(cty.String), - cty.UnknownVal(cty.Number), - }, - { - cty.DynamicVal, - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).Refine(). + NotNull(). + NumberRangeLowerBound(cty.Zero, true). + NewValue(), }, } diff --git a/hcl2template/function/refinements.go b/hcl2template/function/refinements.go new file mode 100644 index 00000000000..99314d7ee48 --- /dev/null +++ b/hcl2template/function/refinements.go @@ -0,0 +1,9 @@ +package function + +import ( + "github.com/zclconf/go-cty/cty" +) + +func refineNotNull(b *cty.RefinementBuilder) *cty.RefinementBuilder { + return b.NotNull() +}