jsonconfig: properly unwind and enumerate references (#28884)

The "references" included in the expression representation now properly unwrap for each traversal step, to match what was documented.
This commit is contained in:
Kristin Laemmert 2021-06-14 09:22:22 -04:00 committed by GitHub
parent ac03d35997
commit 329585d07d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 135 additions and 29 deletions

View File

@ -1,10 +1,13 @@
package jsonconfig
import (
"bytes"
"encoding/json"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang"
"github.com/zclconf/go-cty/cty"
@ -30,22 +33,49 @@ type expression struct {
func marshalExpression(ex hcl.Expression) expression {
var ret expression
if ex != nil {
if ex == nil {
return ret
}
val, _ := ex.Value(nil)
if val != cty.NilVal {
valJSON, _ := ctyjson.Marshal(val, val.Type())
ret.ConstantValue = valJSON
}
vars, _ := lang.ReferencesInExpr(ex)
refs, _ := lang.ReferencesInExpr(ex)
if len(refs) > 0 {
var varString []string
if len(vars) > 0 {
for _, v := range vars {
varString = append(varString, v.Subject.String())
for _, ref := range refs {
// We work backwards here, starting with the full reference +
// reamining traversal, and then unwrapping the remaining traversals
// into parts until we end up at the smallest referencable address.
remains := ref.Remaining
for len(remains) > 0 {
varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, traversalStr(remains)))
remains = remains[:(len(remains) - 1)]
}
varString = append(varString, ref.Subject.String())
switch ref.Subject.(type) {
case addrs.ModuleCallInstance:
if ref.Subject.(addrs.ModuleCallInstance).Key != addrs.NoKey {
// Include the module call, without the key
varString = append(varString, ref.Subject.(addrs.ModuleCallInstance).Call.String())
}
case addrs.ResourceInstance:
if ref.Subject.(addrs.ResourceInstance).Key != addrs.NoKey {
// Include the resource, without the key
varString = append(varString, ref.Subject.(addrs.ResourceInstance).Resource.String())
}
case addrs.AbsModuleCallOutput:
// Include the module name, without the output name
varString = append(varString, ref.Subject.(addrs.AbsModuleCallOutput).Call.String())
}
}
ret.References = varString
}
return ret
}
return ret
}
@ -117,3 +147,31 @@ func marshalExpressions(body hcl.Body, schema *configschema.Block) expressions {
return ret
}
// traversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
//
// This was copied (and simplified) from internal/command/views/json/diagnostic.go.
func traversalStr(traversal hcl.Traversal) string {
var buf bytes.Buffer
for _, step := range traversal {
switch tStep := step.(type) {
case hcl.TraverseRoot:
buf.WriteString(tStep.Name)
case hcl.TraverseAttr:
buf.WriteByte('.')
buf.WriteString(tStep.Name)
case hcl.TraverseIndex:
buf.WriteByte('[')
switch tStep.Key.Type() {
case cty.String:
buf.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
case cty.Number:
bf := tStep.Key.AsBigFloat()
buf.WriteString(bf.Text('g', 10))
}
buf.WriteByte(']')
}
}
return buf.String()
}

View File

@ -9,13 +9,13 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/hashicorp/terraform/internal/configs/configschema"
)
func TestMarshalExpressions(t *testing.T) {
tests := []struct {
Input hcl.Body
Schema *configschema.Block
Want expressions
}{
{
@ -28,14 +28,6 @@ func TestMarshalExpressions(t *testing.T) {
},
},
},
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
expressions{
"foo": expression{
ConstantValue: json.RawMessage([]byte(`"bar"`)),
@ -43,12 +35,66 @@ func TestMarshalExpressions(t *testing.T) {
},
},
},
{
hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"foo": {
Name: "foo",
Expr: hcltest.MockExprTraversalSrc(`var.list[1]`),
},
},
}),
expressions{
"foo": expression{
References: []string{"var.list[1]", "var.list"},
},
},
},
{
hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"foo": {
Name: "foo",
Expr: hcltest.MockExprTraversalSrc(`data.template_file.foo[1].vars["baz"]`),
},
},
}),
expressions{
"foo": expression{
References: []string{"data.template_file.foo[1].vars[\"baz\"]", "data.template_file.foo[1].vars", "data.template_file.foo[1]", "data.template_file.foo"},
},
},
},
{
hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"foo": {
Name: "foo",
Expr: hcltest.MockExprTraversalSrc(`module.foo.bar`),
},
},
}),
expressions{
"foo": expression{
References: []string{"module.foo.bar", "module.foo"},
},
},
},
}
for _, test := range tests {
got := marshalExpressions(test.Input, test.Schema)
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
}
got := marshalExpressions(test.Input, schema)
if !reflect.DeepEqual(got, test.Want) {
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
t.Errorf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}

View File

@ -334,7 +334,7 @@ func TestShow_json_output(t *testing.T) {
expectError := strings.Contains(entry.Name(), "error")
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": []string{"1.2.3"},
"test": {"1.2.3"},
})
defer close()

View File

@ -196,7 +196,8 @@
"test": {
"expression": {
"references": [
"module.module_test_foo.test"
"module.module_test_foo.test",
"module.module_test_foo"
]
},
"depends_on": [

View File

@ -86,6 +86,7 @@
"test": {
"expression": {
"references": [
"test_instance.test.ami",
"test_instance.test"
]
},