From 329585d07d1d18ced0f5c02a51775a955f3c00d5 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Mon, 14 Jun 2021 09:22:22 -0400 Subject: [PATCH] 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. --- internal/command/jsonconfig/expression.go | 86 ++++++++++++++++--- .../command/jsonconfig/expression_test.go | 72 +++++++++++++--- internal/command/show_test.go | 2 +- .../testdata/show-json/modules/output.json | 3 +- .../show-json/sensitive-values/output.json | 1 + 5 files changed, 135 insertions(+), 29 deletions(-) diff --git a/internal/command/jsonconfig/expression.go b/internal/command/jsonconfig/expression.go index 1ba27eb03..52ff927c2 100644 --- a/internal/command/jsonconfig/expression.go +++ b/internal/command/jsonconfig/expression.go @@ -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 { - val, _ := ex.Value(nil) - if val != cty.NilVal { - valJSON, _ := ctyjson.Marshal(val, val.Type()) - ret.ConstantValue = valJSON - } - vars, _ := lang.ReferencesInExpr(ex) - var varString []string - if len(vars) > 0 { - for _, v := range vars { - varString = append(varString, v.Subject.String()) - } - ret.References = varString - } + if ex == nil { return ret } + + val, _ := ex.Value(nil) + if val != cty.NilVal { + valJSON, _ := ctyjson.Marshal(val, val.Type()) + ret.ConstantValue = valJSON + } + + refs, _ := lang.ReferencesInExpr(ex) + if len(refs) > 0 { + var varString []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 } @@ -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() +} diff --git a/internal/command/jsonconfig/expression_test.go b/internal/command/jsonconfig/expression_test.go index 88b72d4d0..971fb78d4 100644 --- a/internal/command/jsonconfig/expression_test.go +++ b/internal/command/jsonconfig/expression_test.go @@ -9,14 +9,14 @@ 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 + Input hcl.Body + Want expressions }{ { &hclsyntax.Body{ @@ -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) } } } diff --git a/internal/command/show_test.go b/internal/command/show_test.go index a89d15dea..b5ed87797 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -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() diff --git a/internal/command/testdata/show-json/modules/output.json b/internal/command/testdata/show-json/modules/output.json index d4cacdaff..440bebbff 100644 --- a/internal/command/testdata/show-json/modules/output.json +++ b/internal/command/testdata/show-json/modules/output.json @@ -196,7 +196,8 @@ "test": { "expression": { "references": [ - "module.module_test_foo.test" + "module.module_test_foo.test", + "module.module_test_foo" ] }, "depends_on": [ diff --git a/internal/command/testdata/show-json/sensitive-values/output.json b/internal/command/testdata/show-json/sensitive-values/output.json index d3920743c..7cbc9ccf0 100644 --- a/internal/command/testdata/show-json/sensitive-values/output.json +++ b/internal/command/testdata/show-json/sensitive-values/output.json @@ -86,6 +86,7 @@ "test": { "expression": { "references": [ + "test_instance.test.ami", "test_instance.test" ] },