From ae3c0c6a4a609522614d3d6aabed599c52492c54 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 4 Nov 2020 13:18:44 -0800 Subject: [PATCH] lang/funcs: Remove the deprecated "list" and "map" functions Prior to Terraform 0.12 these two functions were the only way to construct literal lists and maps (respectively) in HIL expressions. Terraform 0.12, by switching to HCL 2, introduced first-class syntax for constructing tuple and object values, which can then be converted into list and map values using the tolist and tomap type conversion functions. We marked both of these functions as deprecated in the Terraform v0.12 release and have since then mentioned in the docs that they will be removed in a future Terraform version. The "terraform 0.12upgrade" tool from Terraform v0.12 also included a rule to automatically rewrite uses of these functions into equivalent new syntax. The main motivation for removing these now is just to get this change made prior to Terraform 1.0. as we'll be doing with various other deprecations. However, a specific reason for these two functions in particular is that their existence is what caused us to invent the idea of a "type expression" as a distinct kind of expression in Terraform v0.12, and so removing them now would allow potentially unifying type expressions with value expressions in a future release. We do not have any current specific plans to make that change, but one potential motivation for doing so would be to take another attempt at a generalized "convert" function which takes a type as one of its arguments. Our previous attempt to implement such a function was foiled by the fact that Terraform's expression validator doesn't have any way to know to treat one argument of a particular function as special, and so it was generating incorrect error messages. We won't necessarily do that, but having these "list" and "map" functions out of the way leaves the option open. --- lang/funcs/collection.go | 161 +++--------- lang/funcs/collection_test.go | 241 ------------------ lang/functions_test.go | 20 +- .../testdata/apply-resource-scale-in/main.tf | 2 +- terraform/testdata/plan-for-each/main.tf | 2 +- .../docs/configuration/functions/list.html.md | 37 +-- .../docs/configuration/functions/map.html.md | 38 +-- 7 files changed, 75 insertions(+), 426 deletions(-) diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index cca7423c0..0b5922cb1 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -220,50 +220,6 @@ func flattener(flattenList cty.Value) ([]cty.Value, bool) { return out, true } -// ListFunc constructs a function that takes an arbitrary number of arguments -// and returns a list containing those values in the same order. -// -// This function is deprecated in Terraform v0.12 -var ListFunc = function.New(&function.Spec{ - Params: []function.Parameter{}, - VarParam: &function.Parameter{ - Name: "vals", - Type: cty.DynamicPseudoType, - AllowUnknown: true, - AllowDynamicType: true, - AllowNull: true, - }, - Type: func(args []cty.Value) (ret cty.Type, err error) { - if len(args) == 0 { - return cty.NilType, errors.New("at least one argument is required") - } - - argTypes := make([]cty.Type, len(args)) - - for i, arg := range args { - argTypes[i] = arg.Type() - } - - retType, _ := convert.UnifyUnsafe(argTypes) - if retType == cty.NilType { - return cty.NilType, errors.New("all arguments must have the same type") - } - - return cty.List(retType), nil - }, - Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - newList := make([]cty.Value, 0, len(args)) - - for _, arg := range args { - // We already know this will succeed because of the checks in our Type func above - arg, _ = convert.Convert(arg, retType.ElementType()) - newList = append(newList, arg) - } - - return cty.ListVal(newList), nil - }, -}) - // LookupFunc constructs a function that performs dynamic lookups of map types. var LookupFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -354,81 +310,6 @@ var LookupFunc = function.New(&function.Spec{ }, }) -// MapFunc constructs a function that takes an even number of arguments and -// returns a map whose elements are constructed from consecutive pairs of arguments. -// -// This function is deprecated in Terraform v0.12 -var MapFunc = function.New(&function.Spec{ - Params: []function.Parameter{}, - VarParam: &function.Parameter{ - Name: "vals", - Type: cty.DynamicPseudoType, - AllowUnknown: true, - AllowDynamicType: true, - AllowNull: true, - }, - Type: func(args []cty.Value) (ret cty.Type, err error) { - if len(args) < 2 || len(args)%2 != 0 { - return cty.NilType, fmt.Errorf("map requires an even number of two or more arguments, got %d", len(args)) - } - - argTypes := make([]cty.Type, len(args)/2) - index := 0 - - for i := 0; i < len(args); i += 2 { - argTypes[index] = args[i+1].Type() - index++ - } - - valType, _ := convert.UnifyUnsafe(argTypes) - if valType == cty.NilType { - return cty.NilType, errors.New("all arguments must have the same type") - } - - return cty.Map(valType), nil - }, - Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - for _, arg := range args { - if !arg.IsWhollyKnown() { - return cty.UnknownVal(retType), nil - } - } - - outputMap := make(map[string]cty.Value) - - for i := 0; i < len(args); i += 2 { - - keyVal, err := convert.Convert(args[i], cty.String) - if err != nil { - return cty.NilVal, err - } - if keyVal.IsNull() { - return cty.NilVal, fmt.Errorf("argument %d is a null key", i+1) - } - key := keyVal.AsString() - - val := args[i+1] - - var variable cty.Value - err = gocty.FromCtyValue(val, &variable) - if err != nil { - return cty.NilVal, err - } - - // We already know this will succeed because of the checks in our Type func above - variable, _ = convert.Convert(variable, retType.ElementType()) - - // Check for duplicate keys - if _, ok := outputMap[key]; ok { - return cty.NilVal, fmt.Errorf("argument %d is a duplicate key: %q", i+1, key) - } - outputMap[key] = variable - } - - return cty.MapVal(outputMap), nil - }, -}) - // MatchkeysFunc constructs a function that constructs a new list by taking a // subset of elements from one list whose indexes match the corresponding // indexes of values in another list. @@ -614,6 +495,48 @@ var TransposeFunc = function.New(&function.Spec{ }, }) +// ListFunc constructs a function that takes an arbitrary number of arguments +// and returns a list containing those values in the same order. +// +// This function is deprecated in Terraform v0.12 +var ListFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "vals", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + }, + Type: func(args []cty.Value) (ret cty.Type, err error) { + return cty.DynamicPseudoType, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list") + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + return cty.DynamicVal, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list") + }, +}) + +// MapFunc constructs a function that takes an even number of arguments and +// returns a map whose elements are constructed from consecutive pairs of arguments. +// +// This function is deprecated in Terraform v0.12 +var MapFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "vals", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + }, + Type: func(args []cty.Value) (ret cty.Type, err error) { + return cty.DynamicPseudoType, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map") + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + return cty.DynamicVal, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map") + }, +}) + // helper function to add an element to a list, if it does not already exist func appendIfMissing(slice []cty.Value, element cty.Value) ([]cty.Value, error) { for _, ele := range slice { diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index f74b3aafc..89cb2a15f 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/function" ) func TestLength(t *testing.T) { @@ -472,83 +471,6 @@ func TestIndex(t *testing.T) { } } -func TestList(t *testing.T) { - tests := []struct { - Values []cty.Value - Want cty.Value - Err bool - }{ - { - []cty.Value{ - cty.NilVal, - }, - cty.NilVal, - true, - }, - { - []cty.Value{ - cty.StringVal("Hello"), - }, - cty.ListVal([]cty.Value{ - cty.StringVal("Hello"), - }), - false, - }, - { - []cty.Value{ - cty.StringVal("Hello"), - cty.StringVal("World"), - }, - cty.ListVal([]cty.Value{ - cty.StringVal("Hello"), - cty.StringVal("World"), - }), - false, - }, - { - []cty.Value{ - cty.StringVal("Hello"), - cty.NumberIntVal(42), - }, - cty.ListVal([]cty.Value{ - cty.StringVal("Hello"), - cty.StringVal("42"), - }), - false, - }, - { - []cty.Value{ - cty.StringVal("Hello"), - cty.UnknownVal(cty.String), - }, - cty.ListVal([]cty.Value{ - cty.StringVal("Hello"), - cty.UnknownVal(cty.String), - }), - false, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("list(%#v)", test.Values), func(t *testing.T) { - got, err := List(test.Values...) - - if test.Err { - if err == nil { - t.Fatal("succeeded; want error") - } - return - } else if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - if !got.RawEquals(test.Want) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) - } - }) - } -} - func TestLookup(t *testing.T) { simpleMap := cty.MapVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), @@ -802,169 +724,6 @@ func TestLookup(t *testing.T) { } } -func TestMap(t *testing.T) { - tests := []struct { - Values []cty.Value - Want cty.Value - Err bool - }{ - { - []cty.Value{ - cty.StringVal("hello"), - cty.StringVal("world"), - }, - cty.MapVal(map[string]cty.Value{ - "hello": cty.StringVal("world"), - }), - false, - }, - { - []cty.Value{ - cty.StringVal("hello"), - cty.UnknownVal(cty.String), - }, - cty.UnknownVal(cty.Map(cty.String)), - false, - }, - { - []cty.Value{ - cty.StringVal("hello"), - cty.StringVal("world"), - cty.StringVal("what's"), - cty.StringVal("up"), - }, - cty.MapVal(map[string]cty.Value{ - "hello": cty.StringVal("world"), - "what's": cty.StringVal("up"), - }), - false, - }, - { - []cty.Value{ - cty.StringVal("hello"), - cty.NumberIntVal(1), - cty.StringVal("goodbye"), - cty.NumberIntVal(42), - }, - cty.MapVal(map[string]cty.Value{ - "hello": cty.NumberIntVal(1), - "goodbye": cty.NumberIntVal(42), - }), - false, - }, - { // convert numbers to strings - []cty.Value{ - cty.StringVal("hello"), - cty.NumberIntVal(1), - cty.StringVal("goodbye"), - cty.StringVal("42"), - }, - cty.MapVal(map[string]cty.Value{ - "hello": cty.StringVal("1"), - "goodbye": cty.StringVal("42"), - }), - false, - }, - { // convert number keys to strings - []cty.Value{ - cty.NumberIntVal(1), - cty.StringVal("hello"), - cty.NumberIntVal(2), - cty.StringVal("goodbye"), - }, - cty.MapVal(map[string]cty.Value{ - "1": cty.StringVal("hello"), - "2": cty.StringVal("goodbye"), - }), - false, - }, - { // map of lists is okay - []cty.Value{ - cty.StringVal("hello"), - cty.ListVal([]cty.Value{ - cty.StringVal("world"), - }), - cty.StringVal("what's"), - cty.ListVal([]cty.Value{ - cty.StringVal("up"), - }), - }, - cty.MapVal(map[string]cty.Value{ - "hello": cty.ListVal([]cty.Value{cty.StringVal("world")}), - "what's": cty.ListVal([]cty.Value{cty.StringVal("up")}), - }), - false, - }, - { // map of maps is okay - []cty.Value{ - cty.StringVal("hello"), - cty.MapVal(map[string]cty.Value{ - "there": cty.StringVal("world"), - }), - cty.StringVal("what's"), - cty.MapVal(map[string]cty.Value{ - "really": cty.StringVal("up"), - }), - }, - cty.MapVal(map[string]cty.Value{ - "hello": cty.MapVal(map[string]cty.Value{ - "there": cty.StringVal("world"), - }), - "what's": cty.MapVal(map[string]cty.Value{ - "really": cty.StringVal("up"), - }), - }), - false, - }, - { // single argument returns an error - []cty.Value{ - cty.StringVal("hello"), - }, - cty.NilVal, - true, - }, - { // duplicate keys returns an error - []cty.Value{ - cty.StringVal("hello"), - cty.StringVal("world"), - cty.StringVal("hello"), - cty.StringVal("universe"), - }, - cty.NilVal, - true, - }, - { // null key returns an error - []cty.Value{ - cty.NullVal(cty.DynamicPseudoType), - cty.NumberIntVal(5), - }, - cty.NilVal, - true, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("map(%#v)", test.Values), func(t *testing.T) { - got, err := Map(test.Values...) - if test.Err { - if err == nil { - t.Fatal("succeeded; want error") - } - if _, ok := err.(function.PanicError); ok { - t.Fatalf("unexpected panic: %s", err) - } - return - } else if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - if !got.RawEquals(test.Want) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) - } - }) - } -} - func TestMatchkeys(t *testing.T) { tests := []struct { Keys cty.Value diff --git a/lang/functions_test.go b/lang/functions_test.go index 4690a4a05..46be8649f 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -224,7 +224,7 @@ func TestFunctions(t *testing.T) { "coalescelist": { { - `coalescelist(list("a", "b"), list("c", "d"))`, + `coalescelist(tolist(["a", "b"]), tolist(["c", "d"]))`, cty.ListVal([]cty.Value{ cty.StringVal("a"), cty.StringVal("b"), @@ -512,12 +512,8 @@ func TestFunctions(t *testing.T) { }, "list": { - { - `list("hello")`, - cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - }), - }, + // There are intentionally no test cases for "list" because + // it is a stub that always returns an error. }, "log": { @@ -542,12 +538,8 @@ func TestFunctions(t *testing.T) { }, "map": { - { - `map("hello", "world")`, - cty.MapVal(map[string]cty.Value{ - "hello": cty.StringVal("world"), - }), - }, + // There are intentionally no test cases for "map" because + // it is a stub that always returns an error. }, "matchkeys": { @@ -759,7 +751,7 @@ func TestFunctions(t *testing.T) { "slice": { { // force a list type here for testing - `slice(list("a", "b", "c", "d"), 1, 3)`, + `slice(tolist(["a", "b", "c", "d"]), 1, 3)`, cty.ListVal([]cty.Value{ cty.StringVal("b"), cty.StringVal("c"), }), diff --git a/terraform/testdata/apply-resource-scale-in/main.tf b/terraform/testdata/apply-resource-scale-in/main.tf index 0363d89b7..8cb38473e 100644 --- a/terraform/testdata/apply-resource-scale-in/main.tf +++ b/terraform/testdata/apply-resource-scale-in/main.tf @@ -5,7 +5,7 @@ resource "aws_instance" "one" { } locals { - one_id = element(concat(aws_instance.one.*.id, list("")), 0) + one_id = element(concat(aws_instance.one.*.id, [""]), 0) } resource "aws_instance" "two" { diff --git a/terraform/testdata/plan-for-each/main.tf b/terraform/testdata/plan-for-each/main.tf index bffb079cb..94572e20a 100644 --- a/terraform/testdata/plan-for-each/main.tf +++ b/terraform/testdata/plan-for-each/main.tf @@ -13,7 +13,7 @@ resource "aws_instance" "bar" { for_each = toset([]) } resource "aws_instance" "bar2" { - for_each = toset(list("z", "y", "x")) + for_each = toset(["z", "y", "x"]) } # an empty map should generate no resource diff --git a/website/docs/configuration/functions/list.html.md b/website/docs/configuration/functions/list.html.md index 0313bac13..cb6eabb9c 100644 --- a/website/docs/configuration/functions/list.html.md +++ b/website/docs/configuration/functions/list.html.md @@ -12,37 +12,22 @@ description: |- earlier, see [0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). -~> **This function is deprecated.** From Terraform v0.12, the Terraform -language has built-in syntax for creating lists using the `[` and `]` -delimiters. Use the built-in syntax instead. The `list` function will be -removed in a future version of Terraform. +The `list` function is no longer available. Prior to Terraform v0.12 it was +the only available syntax for writing a literal list inside an expression, +but Terraform v0.12 introduced a new first-class syntax. -`list` takes an arbitrary number of arguments and returns a list containing -those values in the same order. - -## Examples +To update an expression like `list(a, b, c)`, write the following instead: ``` -> list("a", "b", "c") -[ - "a", - "b", - "c", -] +tolist([a, b, c]) ``` -Do not use the above form in Terraform v0.12 or above. Instead, use the -built-in list construction syntax, which achieves the same result: - -``` -> ["a", "b", "c"] -[ - "a", - "b", - "c", -] -``` +The `[ ... ]` brackets construct a tuple value, and then the `tolist` function +then converts it to a list. For more information on the value types in the +Terraform language, see [Type Constraints](../types.html). ## Related Functions -* [`tolist`](./tolist.html) converts a set value to a list. +* [`concat`](./concat.html) produces a new list by concatenating together the + elements from other lists. +* [`tolist`](./tolist.html) converts a set or tuple value to a list. diff --git a/website/docs/configuration/functions/map.html.md b/website/docs/configuration/functions/map.html.md index 4735b8788..58c3e0909 100644 --- a/website/docs/configuration/functions/map.html.md +++ b/website/docs/configuration/functions/map.html.md @@ -12,35 +12,25 @@ description: |- earlier, see [0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). -~> **This function is deprecated.** From Terraform v0.12, the Terraform -language has built-in syntax for creating maps using the `{` and `}` -delimiters. Use the built-in syntax instead. The `map` function will be -removed in a future version of Terraform. +The `map` function is no longer available. Prior to Terraform v0.12 it was +the only available syntax for writing a literal map inside an expression, +but Terraform v0.12 introduced a new first-class syntax. -`map` takes an even number of arguments and returns a map whose elements -are constructed from consecutive pairs of arguments. - -## Examples +To update an expression like `map("a", "b", "c", "d")`, write the following instead: ``` -> map("a", "b", "c", "d") -{ - "a" = "b" - "c" = "d" -} +tomap({ + a = "b" + c = "d" +}) ``` -Do not use the above form in Terraform v0.12 or above. Instead, use the -built-in map construction syntax, which achieves the same result: - -``` -> {"a" = "b", "c" = "d"} -{ - "a" = "b" - "c" = "d" -} -``` +The `{ ... }` braces construct an object value, and then the `tomap` function +then converts it to a map. For more information on the value types in the +Terraform language, see [Type Constraints](../types.html). ## Related Functions -* [`tomap`](./tomap.html) performs a type conversion to a map type. +* [`tomap`](./tomap.html) converts an object value to a map. +* [`zipmap`](./zipmap.html) constructs a map dynamically, by taking keys from + one list and values from another list.