From f6af7b4f7a29874775995d2b7bbf546fc6854513 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Fri, 23 Apr 2021 10:29:50 -0400 Subject: [PATCH] lang/funcs: add (console-only) TypeFunction (#28501) * lang/funcs: add (console-only) TypeFunction The type() function, which is only available for terraform console, prints out the type of a given value. This is mainly intended for debugging - it's nice to be able to print out terraform's understanding of a complex variable. This introduces a new field for Scope: ConsoleMode. When ConsoleMode is true, any additional functions intended for use in the console (only) may be added. --- command/console.go | 4 + go.sum | 2 - lang/funcs/conversion.go | 128 ++++++++++++++++++ lang/funcs/conversion_test.go | 90 ++++++++++++ lang/functions.go | 5 + lang/scope.go | 4 + repl/format.go | 6 +- .../docs/configuration/functions/type.html.md | 82 +++++++++++ 8 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 website/docs/configuration/functions/type.html.md diff --git a/command/console.go b/command/console.go index fab428aa6..ebecd9edf 100644 --- a/command/console.go +++ b/command/console.go @@ -127,6 +127,10 @@ func (c *ConsoleCommand) Run(args []string) int { c.showDiagnostics(diags) return 1 } + + // set the ConsoleMode to true so any available console-only functions included. + scope.ConsoleMode = true + if diags.HasErrors() { diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results.")) } diff --git a/go.sum b/go.sum index eaeb380e2..84b6ca85f 100644 --- a/go.sum +++ b/go.sum @@ -595,8 +595,6 @@ github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zclconf/go-cty v1.8.1 h1:SI0LqNeNxAgv2WWqWJMlG2/Ad/6aYJ7IVYYMigmfkuI= -github.com/zclconf/go-cty v1.8.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.8.2 h1:u+xZfBKgpycDnTNjPhGiTEYZS5qS/Sb5MqSfm7vzcjg= github.com/zclconf/go-cty v1.8.2/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= diff --git a/lang/funcs/conversion.go b/lang/funcs/conversion.go index 4991f60a8..b557bb5a9 100644 --- a/lang/funcs/conversion.go +++ b/lang/funcs/conversion.go @@ -1,7 +1,10 @@ package funcs import ( + "fmt" + "sort" "strconv" + "strings" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" @@ -91,3 +94,128 @@ func MakeToFunc(wantTy cty.Type) function.Function { }, }) } + +var TypeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowDynamicType: true, + AllowUnknown: true, + AllowNull: true, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(TypeString(args[0].Type())).Mark("raw"), 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/lang/funcs/conversion_test.go b/lang/funcs/conversion_test.go index 819a0e4d0..97460f5b5 100644 --- a/lang/funcs/conversion_test.go +++ b/lang/funcs/conversion_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" ) @@ -177,3 +178,92 @@ 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/lang/functions.go b/lang/functions.go index aa182d17e..21be9a7f1 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -152,6 +152,11 @@ func (s *Scope) Functions() map[string]function.Function { return s.funcs }) + if s.ConsoleMode { + // The type function is only available in terraform console. + s.funcs["type"] = funcs.TypeFunc + } + if s.PureOnly { // Force our few impure functions to return unknown so that we // can defer evaluating them until a later pass. diff --git a/lang/scope.go b/lang/scope.go index 103d2529c..3a34e9ca2 100644 --- a/lang/scope.go +++ b/lang/scope.go @@ -37,6 +37,10 @@ type Scope struct { // considered as active in the module that this scope will be used for. // Callers can populate it by calling the SetActiveExperiments method. activeExperiments experiments.Set + + // ConsoleMode can be set to true to request any console-only functions are + // included in this scope. + ConsoleMode bool } // SetActiveExperiments allows a caller to declare that a set of experiments diff --git a/repl/format.go b/repl/format.go index 14e09d708..c83e9ef2e 100644 --- a/repl/format.go +++ b/repl/format.go @@ -16,7 +16,11 @@ func FormatValue(v cty.Value, indent int) string { if !v.IsKnown() { return "(known after apply)" } - if v.IsMarked() { + if v.Type().Equals(cty.String) && v.HasMark("raw") { + raw, _ := v.Unmark() + return raw.AsString() + } + if v.HasMark("sensitive") { return "(sensitive)" } if v.IsNull() { diff --git a/website/docs/configuration/functions/type.html.md b/website/docs/configuration/functions/type.html.md new file mode 100644 index 000000000..ed4b0c356 --- /dev/null +++ b/website/docs/configuration/functions/type.html.md @@ -0,0 +1,82 @@ +--- +layout: "language" +page_title: "type - Functions - Configuration Language" +sidebar_current: "docs-funcs-conversion-type" +description: |- + The type function returns the type of a given value. +--- + +# `type` Function + +-> **Note:** This function is available only in Terraform 1.0 and later. + +`type` retuns the type of a given value. + +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. + +## Examples + +Here we have a conditional `output` which prints either the value of `var.list` or a local named `default_list`: + +```hcl +variable "list" { + default = [] +} + +locals { + default_list = [ + { + foo = "bar" + map = { bleep = "bloop" } + }, + { + beep = "boop" + }, + ] +} + +output "list" { + value = var.list != [] ? var.list : local.default_list +} +``` + +Applying this configuration results in the following error: + +``` +Error: Inconsistent conditional result types + + on main.tf line 18, in output "list": + 18: value = var.list != [] ? var.list : local.default_list + |---------------- + | local.default_list is tuple with 2 elements + | var.list is empty tuple + +The true and false result expressions must have consistent types. The given +expressions are tuple and tuple, respectively. +``` + +While this error message does include some type information, it can be helpful +to inspect the exact type that Terraform has determined for each given input. +Examining both `var.list` and `local.default_list` using the `type` function +provides more context for the error message: + +``` +> type(var.list) +tuple +> type(local.default_list) +tuple([ + object({ + foo: string, + map: object({ + bleep: string, + }), + }), + object({ + beep: string, + }), +]) +```