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)
|
||||
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."))
|
||||
}
|
||||
|
|
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.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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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