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.
This commit is contained in:
parent
15b6a1614c
commit
f6af7b4f7a
|
@ -127,6 +127,10 @@ func (c *ConsoleCommand) Run(args []string) int {
|
||||||
c.showDiagnostics(diags)
|
c.showDiagnostics(diags)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set the ConsoleMode to true so any available console-only functions included.
|
||||||
|
scope.ConsoleMode = true
|
||||||
|
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results."))
|
diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results."))
|
||||||
}
|
}
|
||||||
|
|
2
go.sum
2
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.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.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.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 h1:u+xZfBKgpycDnTNjPhGiTEYZS5qS/Sb5MqSfm7vzcjg=
|
||||||
github.com/zclconf/go-cty v1.8.2/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
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=
|
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI=
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package funcs
|
package funcs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
"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)
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -152,6 +152,11 @@ func (s *Scope) Functions() map[string]function.Function {
|
||||||
return s.funcs
|
return s.funcs
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if s.ConsoleMode {
|
||||||
|
// The type function is only available in terraform console.
|
||||||
|
s.funcs["type"] = funcs.TypeFunc
|
||||||
|
}
|
||||||
|
|
||||||
if s.PureOnly {
|
if s.PureOnly {
|
||||||
// Force our few impure functions to return unknown so that we
|
// Force our few impure functions to return unknown so that we
|
||||||
// can defer evaluating them until a later pass.
|
// can defer evaluating them until a later pass.
|
||||||
|
|
|
@ -37,6 +37,10 @@ type Scope struct {
|
||||||
// considered as active in the module that this scope will be used for.
|
// considered as active in the module that this scope will be used for.
|
||||||
// Callers can populate it by calling the SetActiveExperiments method.
|
// Callers can populate it by calling the SetActiveExperiments method.
|
||||||
activeExperiments experiments.Set
|
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
|
// SetActiveExperiments allows a caller to declare that a set of experiments
|
||||||
|
|
|
@ -16,7 +16,11 @@ func FormatValue(v cty.Value, indent int) string {
|
||||||
if !v.IsKnown() {
|
if !v.IsKnown() {
|
||||||
return "(known after apply)"
|
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)"
|
return "(sensitive)"
|
||||||
}
|
}
|
||||||
if v.IsNull() {
|
if v.IsNull() {
|
||||||
|
|
|
@ -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,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
```
|
Loading…
Reference in New Issue