Skip to content

Commit

Permalink
lang/funcs: add (console-only) TypeFunction (#28501)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
mildwonkey committed Apr 23, 2021
1 parent 15b6a16 commit f6af7b4
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 3 deletions.
4 changes: 4 additions & 0 deletions command/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -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."))
}
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
128 changes: 128 additions & 0 deletions lang/funcs/conversion.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package funcs

import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
Expand Down Expand Up @@ -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)
}
90 changes: 90 additions & 0 deletions lang/funcs/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
)

Expand Down Expand Up @@ -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))
}
}
}
5 changes: 5 additions & 0 deletions lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions lang/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion repl/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
82 changes: 82 additions & 0 deletions website/docs/configuration/functions/type.html.md
Original file line number Diff line number Diff line change
@@ -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,
}),
])
```

0 comments on commit f6af7b4

Please sign in to comment.