Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lang/funcs: add (console-only) TypeFunction #28501

Merged
merged 2 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
}),
])
```