From 691b98b612e54bfd7d43adbac48d106d6f869a66 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Fri, 4 Feb 2022 10:32:06 -0500 Subject: [PATCH 1/4] cli: Prevent overuse of console-only type function The console-only `type` function allows interrogation of any value's type. An implementation quirk is that we use a cty.Mark to allow the console to display this type information without the usual HCL quoting. For example: > type("boop") string instead of: > type("boop") "string" Because these marks can propagate when used in complex expressions, using the type function as part of a complex expression could result in this "print as raw" mark being attached to a collection. When this happened, it would result in a crash when we tried to iterate over a marked value. The `type` function was never intended to be used in this way, which is why its use is limited to the console command. Its purpose was as a pseudo-builtin, used only at the top level to display the type of a given value. This commit goes some way to preventing the use of the `type` function in complex expressions, by refusing to display any non-string value which was marked by `type`, or contains a sub-value which was so marked. --- internal/repl/format.go | 4 ---- internal/repl/session.go | 24 ++++++++++++++++++++++++ internal/repl/session_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/internal/repl/format.go b/internal/repl/format.go index 70fb66abf..fbdd44f58 100644 --- a/internal/repl/format.go +++ b/internal/repl/format.go @@ -17,10 +17,6 @@ func FormatValue(v cty.Value, indent int) string { if !v.IsKnown() { return "(known after apply)" } - if v.Type().Equals(cty.String) && v.HasMark(marks.Raw) { - raw, _ := v.Unmark() - return raw.AsString() - } if v.HasMark(marks.Sensitive) { return "(sensitive)" } diff --git a/internal/repl/session.go b/internal/repl/session.go index a9b7b1b12..41a6c359b 100644 --- a/internal/repl/session.go +++ b/internal/repl/session.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -54,6 +55,29 @@ func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) { return "", diags } + // The raw mark is used only by the console-only `type` function, in order + // to allow display of a string value representation of the type without the + // usual HCL formatting. If we receive a string value with this mark, we do + // not want to format it any further. + // + // Due to mark propagation in cty, calling `type` as part of a larger + // expression can lead to other values being marked, which can in turn lead + // to unpredictable results. If any non-string value has the raw mark, we + // return a diagnostic explaining that this use of `type` is not permitted. + if marks.Contains(val, marks.Raw) { + if val.Type().Equals(cty.String) { + raw, _ := val.Unmark() + return raw.AsString(), diags + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid use of type function", + "The console-only \"type\" function cannot be used as part of an expression.", + )) + return "", diags + } + } + return FormatValue(val, 0), diags } diff --git a/internal/repl/session_test.go b/internal/repl/session_test.go index 7110324e1..bf060f607 100644 --- a/internal/repl/session_test.go +++ b/internal/repl/session_test.go @@ -120,6 +120,20 @@ func TestSession_basicState(t *testing.T) { }, }) }) + + t.Run("type function", func(t *testing.T) { + testSession(t, testSessionTest{ + State: state, + Inputs: []testSessionInput{ + { + Input: "type(test_instance.foo)", + Output: `object({ + id: string, +})`, + }, + }, + }) + }) } func TestSession_stateless(t *testing.T) { @@ -178,6 +192,18 @@ func TestSession_stateless(t *testing.T) { }, }) }) + + t.Run("type function cannot be used in expressions", func(t *testing.T) { + testSession(t, testSessionTest{ + Inputs: []testSessionInput{ + { + Input: `[for i in [1, "two", true]: type(i)]`, + Error: true, + ErrorContains: "Invalid use of type function", + }, + }, + }) + }) } func testSession(t *testing.T, test testSessionTest) { @@ -221,6 +247,9 @@ func testSession(t *testing.T, test testSessionTest) { t.Fatalf("failed to create scope: %s", diags.Err()) } + // Ensure that any console-only functions are available + scope.ConsoleMode = true + // Build the session s := &Session{ Scope: scope, From 903d6f105584d0b91fc3c73ad0d12a3e22d5f545 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Wed, 9 Feb 2022 17:15:53 -0500 Subject: [PATCH 2/4] lang: Remove use of marks.Raw in tests These instances of marks.Raw usage were semantically only testing the properties of combining multiple marks. Testing this with an arbitrary value for the mark is just as valid and clearer. --- internal/lang/funcs/number_test.go | 4 ++-- internal/lang/funcs/redact_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/lang/funcs/number_test.go b/internal/lang/funcs/number_test.go index 260e0127c..6caf19af1 100644 --- a/internal/lang/funcs/number_test.go +++ b/internal/lang/funcs/number_test.go @@ -215,9 +215,9 @@ func TestParseInt(t *testing.T) { ``, }, { - cty.StringVal("128").Mark(marks.Raw), + cty.StringVal("128").Mark("boop"), cty.NumberIntVal(10).Mark(marks.Sensitive), - cty.NumberIntVal(128).WithMarks(cty.NewValueMarks(marks.Raw, marks.Sensitive)), + cty.NumberIntVal(128).WithMarks(cty.NewValueMarks("boop", marks.Sensitive)), ``, }, { diff --git a/internal/lang/funcs/redact_test.go b/internal/lang/funcs/redact_test.go index b45721fb9..e378d5f5a 100644 --- a/internal/lang/funcs/redact_test.go +++ b/internal/lang/funcs/redact_test.go @@ -18,14 +18,14 @@ func TestRedactIfSensitive(t *testing.T) { marks: []cty.ValueMarks{cty.NewValueMarks(marks.Sensitive)}, want: "(sensitive value)", }, - "raw non-sensitive string": { + "marked non-sensitive string": { value: "foo", - marks: []cty.ValueMarks{cty.NewValueMarks(marks.Raw)}, + marks: []cty.ValueMarks{cty.NewValueMarks("boop")}, want: `"foo"`, }, - "raw sensitive string": { + "sensitive string with other marks": { value: "foo", - marks: []cty.ValueMarks{cty.NewValueMarks(marks.Raw), cty.NewValueMarks(marks.Sensitive)}, + marks: []cty.ValueMarks{cty.NewValueMarks("boop"), cty.NewValueMarks(marks.Sensitive)}, want: "(sensitive value)", }, "sensitive number": { From 843c50e8ce815ba233816e06bd68503a771abd93 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Wed, 9 Feb 2022 17:17:14 -0500 Subject: [PATCH 3/4] lang: Further limit the console-only type function This commit introduces a capsule type, `TypeType`, which is used to extricate type information from the console-only `type` function. In combination with the `TypeType` mark, this allows us to restrict the use of this function to top-level display of a value's type. Any other use of `type()` will result in an error diagnostic. --- internal/lang/funcs/conversion.go | 117 ++------------------- internal/lang/funcs/conversion_test.go | 90 ----------------- internal/lang/marks/marks.go | 7 +- internal/lang/types/type_type.go | 12 +++ internal/lang/types/types.go | 2 + internal/repl/session.go | 135 ++++++++++++++++++++++--- internal/repl/session_test.go | 132 ++++++++++++++++++++++++ 7 files changed, 278 insertions(+), 217 deletions(-) create mode 100644 internal/lang/types/type_type.go create mode 100644 internal/lang/types/types.go diff --git a/internal/lang/funcs/conversion.go b/internal/lang/funcs/conversion.go index b0f9d2a6b..8eebb3a62 100644 --- a/internal/lang/funcs/conversion.go +++ b/internal/lang/funcs/conversion.go @@ -1,12 +1,10 @@ package funcs import ( - "fmt" - "sort" "strconv" - "strings" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/lang/types" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/function" @@ -97,6 +95,9 @@ func MakeToFunc(wantTy cty.Type) function.Function { }) } +// TypeFunc returns an encapsulated value containing its argument's type. This +// value is marked to allow us to limit the use of this function at the moment +// to only a few supported use cases. var TypeFunc = function.New(&function.Spec{ Params: []function.Parameter{ { @@ -107,117 +108,13 @@ var TypeFunc = function.New(&function.Spec{ AllowNull: true, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(types.TypeType), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - return cty.StringVal(TypeString(args[0].Type())).Mark(marks.Raw), nil + givenType := args[0].Type() + return cty.CapsuleVal(types.TypeType, &givenType).Mark(marks.TypeType), nil }, }) -// Modified copy of TypeString from go-cty: -// https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go -// -// TypeString returns a string representation of a given type that is -// reminiscent of Go syntax calling into the cty package but is mainly -// intended for easy human inspection of values in tests, debug output, etc. -// -// The resulting string will include newlines and indentation in order to -// increase the readability of complex structures. It always ends with a -// newline, so you can print this result directly to your output. -func TypeString(ty cty.Type) string { - var b strings.Builder - writeType(ty, &b, 0) - return b.String() -} - -func writeType(ty cty.Type, b *strings.Builder, indent int) { - switch { - case ty == cty.NilType: - b.WriteString("nil") - return - case ty.IsObjectType(): - atys := ty.AttributeTypes() - if len(atys) == 0 { - b.WriteString("object({})") - return - } - attrNames := make([]string, 0, len(atys)) - for name := range atys { - attrNames = append(attrNames, name) - } - sort.Strings(attrNames) - b.WriteString("object({\n") - indent++ - for _, name := range attrNames { - aty := atys[name] - b.WriteString(indentSpaces(indent)) - fmt.Fprintf(b, "%s: ", name) - writeType(aty, b, indent) - b.WriteString(",\n") - } - indent-- - b.WriteString(indentSpaces(indent)) - b.WriteString("})") - case ty.IsTupleType(): - etys := ty.TupleElementTypes() - if len(etys) == 0 { - b.WriteString("tuple([])") - return - } - b.WriteString("tuple([\n") - indent++ - for _, ety := range etys { - b.WriteString(indentSpaces(indent)) - writeType(ety, b, indent) - b.WriteString(",\n") - } - indent-- - b.WriteString(indentSpaces(indent)) - b.WriteString("])") - case ty.IsCollectionType(): - ety := ty.ElementType() - switch { - case ty.IsListType(): - b.WriteString("list(") - case ty.IsMapType(): - b.WriteString("map(") - case ty.IsSetType(): - b.WriteString("set(") - default: - // At the time of writing there are no other collection types, - // but we'll be robust here and just pass through the GoString - // of anything we don't recognize. - b.WriteString(ty.FriendlyName()) - return - } - // Because object and tuple types render split over multiple - // lines, a collection type container around them can end up - // being hard to see when scanning, so we'll generate some extra - // indentation to make a collection of structural type more visually - // distinct from the structural type alone. - complexElem := ety.IsObjectType() || ety.IsTupleType() - if complexElem { - indent++ - b.WriteString("\n") - b.WriteString(indentSpaces(indent)) - } - writeType(ty.ElementType(), b, indent) - if complexElem { - indent-- - b.WriteString(",\n") - b.WriteString(indentSpaces(indent)) - } - b.WriteString(")") - default: - // For any other type we'll just use its GoString and assume it'll - // follow the usual GoString conventions. - b.WriteString(ty.FriendlyName()) - } -} - -func indentSpaces(level int) string { - return strings.Repeat(" ", level) -} - func Type(input []cty.Value) (cty.Value, error) { return TypeFunc.Call(input) } diff --git a/internal/lang/funcs/conversion_test.go b/internal/lang/funcs/conversion_test.go index cec2e23c9..40317ba13 100644 --- a/internal/lang/funcs/conversion_test.go +++ b/internal/lang/funcs/conversion_test.go @@ -4,7 +4,6 @@ import ( "fmt" "testing" - "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) @@ -191,92 +190,3 @@ func TestTo(t *testing.T) { }) } } - -func TestType(t *testing.T) { - tests := []struct { - Input cty.Value - Want string - }{ - // Primititves - { - cty.StringVal("a"), - "string", - }, - { - cty.NumberIntVal(42), - "number", - }, - { - cty.BoolVal(true), - "bool", - }, - // Collections - { - cty.EmptyObjectVal, - `object({})`, - }, - { - cty.EmptyTupleVal, - `tuple([])`, - }, - { - cty.ListValEmpty(cty.String), - `list(string)`, - }, - { - cty.MapValEmpty(cty.String), - `map(string)`, - }, - { - cty.SetValEmpty(cty.String), - `set(string)`, - }, - { - cty.ListVal([]cty.Value{cty.StringVal("a")}), - `list(string)`, - }, - { - cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{cty.NumberIntVal(42)})}), - `list(list(number))`, - }, - { - cty.ListVal([]cty.Value{cty.MapValEmpty(cty.String)}), - `list(map(string))`, - }, - { - cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("bar"), - })}), - "list(\n object({\n foo: string,\n }),\n)", - }, - // Unknowns and Nulls - { - cty.UnknownVal(cty.String), - "string", - }, - { - cty.NullVal(cty.Object(map[string]cty.Type{ - "foo": cty.String, - })), - "object({\n foo: string,\n})", - }, - { // irrelevant marks do nothing - cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("bar").Mark("ignore me"), - })}), - "list(\n object({\n foo: string,\n }),\n)", - }, - } - for _, test := range tests { - got, err := Type([]cty.Value{test.Input}) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - // The value is marked to help with formatting - got, _ = got.Unmark() - - if got.AsString() != test.Want { - t.Errorf("wrong result:\n%s", cmp.Diff(got.AsString(), test.Want)) - } - } -} diff --git a/internal/lang/marks/marks.go b/internal/lang/marks/marks.go index 7e5bb3d0d..0a174d915 100644 --- a/internal/lang/marks/marks.go +++ b/internal/lang/marks/marks.go @@ -38,6 +38,7 @@ func Contains(val cty.Value, mark valueMark) bool { // Terraform. var Sensitive = valueMark("sensitive") -// Raw is used to indicate to the repl that the value should be written without -// any formatting. -var Raw = valueMark("raw") +// TypeType is used to indicate that the value contains a representation of +// another value's type. This is part of the implementation of the console-only +// `type` function. +var TypeType = valueMark("typeType") diff --git a/internal/lang/types/type_type.go b/internal/lang/types/type_type.go new file mode 100644 index 000000000..14edf5ece --- /dev/null +++ b/internal/lang/types/type_type.go @@ -0,0 +1,12 @@ +package types + +import ( + "reflect" + + "github.com/zclconf/go-cty/cty" +) + +// TypeType is a capsule type used to represent a cty.Type as a cty.Value. This +// is used by the `type()` console function to smuggle cty.Type values to the +// REPL session, where it can be displayed to the user directly. +var TypeType = cty.Capsule("type", reflect.TypeOf(cty.Type{})) diff --git a/internal/lang/types/types.go b/internal/lang/types/types.go new file mode 100644 index 000000000..69355d90a --- /dev/null +++ b/internal/lang/types/types.go @@ -0,0 +1,2 @@ +// Package types contains non-standard cty types used only within Terraform. +package types diff --git a/internal/repl/session.go b/internal/repl/session.go index 41a6c359b..f07363ec1 100644 --- a/internal/repl/session.go +++ b/internal/repl/session.go @@ -1,6 +1,8 @@ package repl import ( + "fmt" + "sort" "strings" "github.com/zclconf/go-cty/cty" @@ -9,6 +11,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/lang/types" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -55,20 +58,19 @@ func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) { return "", diags } - // The raw mark is used only by the console-only `type` function, in order - // to allow display of a string value representation of the type without the - // usual HCL formatting. If we receive a string value with this mark, we do - // not want to format it any further. - // - // Due to mark propagation in cty, calling `type` as part of a larger - // expression can lead to other values being marked, which can in turn lead - // to unpredictable results. If any non-string value has the raw mark, we - // return a diagnostic explaining that this use of `type` is not permitted. - if marks.Contains(val, marks.Raw) { - if val.Type().Equals(cty.String) { - raw, _ := val.Unmark() - return raw.AsString(), diags - } else { + // The TypeType mark is used only by the console-only `type` function, in + // order to smuggle the type of a given value back here. We can then + // display a representation of the type directly. + if marks.Contains(val, marks.TypeType) { + val, _ = val.UnmarkDeep() + + valType := val.Type() + switch { + case valType.Equals(types.TypeType): + // An encapsulated type value, which should be displayed directly. + valType := val.EncapsulatedValue().(*cty.Type) + return typeString(*valType), diags + default: diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid use of type function", @@ -96,3 +98,108 @@ Control-D. return strings.TrimSpace(text), nil } + +// Modified copy of TypeString from go-cty: +// https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go +// +// TypeString returns a string representation of a given type that is +// reminiscent of Go syntax calling into the cty package but is mainly +// intended for easy human inspection of values in tests, debug output, etc. +// +// The resulting string will include newlines and indentation in order to +// increase the readability of complex structures. It always ends with a +// newline, so you can print this result directly to your output. +func typeString(ty cty.Type) string { + var b strings.Builder + writeType(ty, &b, 0) + return b.String() +} + +func writeType(ty cty.Type, b *strings.Builder, indent int) { + switch { + case ty == cty.NilType: + b.WriteString("nil") + return + case ty.IsObjectType(): + atys := ty.AttributeTypes() + if len(atys) == 0 { + b.WriteString("object({})") + return + } + attrNames := make([]string, 0, len(atys)) + for name := range atys { + attrNames = append(attrNames, name) + } + sort.Strings(attrNames) + b.WriteString("object({\n") + indent++ + for _, name := range attrNames { + aty := atys[name] + b.WriteString(indentSpaces(indent)) + fmt.Fprintf(b, "%s: ", name) + writeType(aty, b, indent) + b.WriteString(",\n") + } + indent-- + b.WriteString(indentSpaces(indent)) + b.WriteString("})") + case ty.IsTupleType(): + etys := ty.TupleElementTypes() + if len(etys) == 0 { + b.WriteString("tuple([])") + return + } + b.WriteString("tuple([\n") + indent++ + for _, ety := range etys { + b.WriteString(indentSpaces(indent)) + writeType(ety, b, indent) + b.WriteString(",\n") + } + indent-- + b.WriteString(indentSpaces(indent)) + b.WriteString("])") + case ty.IsCollectionType(): + ety := ty.ElementType() + switch { + case ty.IsListType(): + b.WriteString("list(") + case ty.IsMapType(): + b.WriteString("map(") + case ty.IsSetType(): + b.WriteString("set(") + default: + // At the time of writing there are no other collection types, + // but we'll be robust here and just pass through the GoString + // of anything we don't recognize. + b.WriteString(ty.FriendlyName()) + return + } + // Because object and tuple types render split over multiple + // lines, a collection type container around them can end up + // being hard to see when scanning, so we'll generate some extra + // indentation to make a collection of structural type more visually + // distinct from the structural type alone. + complexElem := ety.IsObjectType() || ety.IsTupleType() + if complexElem { + indent++ + b.WriteString("\n") + b.WriteString(indentSpaces(indent)) + } + writeType(ty.ElementType(), b, indent) + if complexElem { + indent-- + b.WriteString(",\n") + b.WriteString(indentSpaces(indent)) + } + b.WriteString(")") + default: + // For any other type we'll just use its GoString and assume it'll + // follow the usual GoString conventions. + b.WriteString(ty.FriendlyName()) + } +} + +func indentSpaces(level int) string { + return strings.Repeat(" ", level) +} diff --git a/internal/repl/session_test.go b/internal/repl/session_test.go index bf060f607..3e976cadc 100644 --- a/internal/repl/session_test.go +++ b/internal/repl/session_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -193,11 +194,59 @@ func TestSession_stateless(t *testing.T) { }) }) + t.Run("type function", func(t *testing.T) { + testSession(t, testSessionTest{ + Inputs: []testSessionInput{ + { + Input: `type("foo")`, + Output: "string", + }, + }, + }) + }) + + t.Run("type type is type", func(t *testing.T) { + testSession(t, testSessionTest{ + Inputs: []testSessionInput{ + { + Input: `type(type("foo"))`, + Output: "type", + }, + }, + }) + }) + + t.Run("interpolating type with strings is not possible", func(t *testing.T) { + testSession(t, testSessionTest{ + Inputs: []testSessionInput{ + { + Input: `"quin${type([])}"`, + Error: true, + ErrorContains: "Invalid template interpolation value", + }, + }, + }) + }) + t.Run("type function cannot be used in expressions", func(t *testing.T) { testSession(t, testSessionTest{ Inputs: []testSessionInput{ { Input: `[for i in [1, "two", true]: type(i)]`, + Output: "", + Error: true, + ErrorContains: "Invalid use of type function", + }, + }, + }) + }) + + t.Run("type equality checks are not permitted", func(t *testing.T) { + testSession(t, testSessionTest{ + Inputs: []testSessionInput{ + { + Input: `type("foo") == type("bar")`, + Output: "", Error: true, ErrorContains: "Invalid use of type function", }, @@ -311,3 +360,86 @@ type testSessionInput struct { Exit bool // Exit is true if exiting is expected ErrorContains string } + +func TestTypeString(t *testing.T) { + tests := []struct { + Input cty.Value + Want string + }{ + // Primititves + { + cty.StringVal("a"), + "string", + }, + { + cty.NumberIntVal(42), + "number", + }, + { + cty.BoolVal(true), + "bool", + }, + // Collections + { + cty.EmptyObjectVal, + `object({})`, + }, + { + cty.EmptyTupleVal, + `tuple([])`, + }, + { + cty.ListValEmpty(cty.String), + `list(string)`, + }, + { + cty.MapValEmpty(cty.String), + `map(string)`, + }, + { + cty.SetValEmpty(cty.String), + `set(string)`, + }, + { + cty.ListVal([]cty.Value{cty.StringVal("a")}), + `list(string)`, + }, + { + cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{cty.NumberIntVal(42)})}), + `list(list(number))`, + }, + { + cty.ListVal([]cty.Value{cty.MapValEmpty(cty.String)}), + `list(map(string))`, + }, + { + cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + })}), + "list(\n object({\n foo: string,\n }),\n)", + }, + // Unknowns and Nulls + { + cty.UnknownVal(cty.String), + "string", + }, + { + cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + })), + "object({\n foo: string,\n})", + }, + { // irrelevant marks do nothing + cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar").Mark("ignore me"), + })}), + "list(\n object({\n foo: string,\n }),\n)", + }, + } + for _, test := range tests { + got := typeString(test.Input.Type()) + if got != test.Want { + t.Errorf("wrong result:\n%s", cmp.Diff(got, test.Want)) + } + } +} From 98f80bc5dad9e7f6441f97c6fd210953f1c34af2 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Wed, 9 Feb 2022 17:41:05 -0500 Subject: [PATCH 4/4] website: Explain limitations of type function --- website/docs/language/functions/type.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/docs/language/functions/type.mdx b/website/docs/language/functions/type.mdx index 28319a8a4..0f742fe7f 100644 --- a/website/docs/language/functions/type.mdx +++ b/website/docs/language/functions/type.mdx @@ -13,7 +13,9 @@ Sometimes a Terraform configuration can result in confusing errors regarding inconsistent types. This function displays terraform's evaluation of a given value's type, which is useful in understanding this error message. -This is a special function which is only available in the `terraform console` command. +This is a special function which is only available in the `terraform console` +command. It can only be used to examine the type of a given value, and should +not be used in more complex expressions. ## Examples