Skip to content

Commit

Permalink
lang: Consider "dynamic" blocks when resolving references
Browse files Browse the repository at this point in the history
The hcldec package has no awareness of the dynamic block extension, so the
hcldec.Variables function misses any variables declared inside dynamic
blocks.

dynblock.VariablesHCLDec is a drop-in replacement for hcldec.Variables
that _is_ aware of dynamic blocks, returning all of the same variables
that hcldec would find naturally plus also any variables used inside
the dynamic block "for_each" and "labels" arguments and inside the
nested "content" block.
  • Loading branch information
apparentlymart committed Mar 19, 2019
1 parent 838a42d commit 50a101a
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 2 deletions.
10 changes: 8 additions & 2 deletions lang/references.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package lang

import (
"github.com/hashicorp/hcl2/ext/dynblock"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/tfdiags"
Expand Down Expand Up @@ -52,7 +52,13 @@ func ReferencesInBlock(body hcl.Body, schema *configschema.Block) ([]*addrs.Refe
return nil, nil
}
spec := schema.DecoderSpec()
traversals := hcldec.Variables(body, spec)

// We use dynblock.VariablesHCLDec instead of hcldec.Variables here because
// when we evaluate a block we'll apply the HCL dynamic block extension
// expansion to it first, and so we need this specialized version in order
// to properly understand what the dependencies will be once expanded.
// Otherwise, we'd miss references that only occur inside dynamic blocks.
traversals := dynblock.VariablesHCLDec(body, spec)
return References(traversals)
}

Expand Down
85 changes: 85 additions & 0 deletions terraform/graph_builder_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/zclconf/go-cty/cty"
)

func TestPlanGraphBuilder_impl(t *testing.T) {
Expand Down Expand Up @@ -67,6 +68,90 @@ func TestPlanGraphBuilder(t *testing.T) {
}
}

func TestPlanGraphBuilder_dynamicBlock(t *testing.T) {
provider := &MockProvider{
GetSchemaReturn: &ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"list": {Type: cty.List(cty.String), Computed: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"nested": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
},
},
},
},
},
},
}
components := &basicComponentFactory{
providers: map[string]providers.Factory{
"test": providers.FactoryFixed(provider),
},
}

b := &PlanGraphBuilder{
Config: testModule(t, "graph-builder-plan-dynblock"),
Components: components,
Schemas: &Schemas{
Providers: map[string]*ProviderSchema{
"test": provider.GetSchemaReturn,
},
},
DisableReduce: true,
}

g, err := b.Build(addrs.RootModuleInstance)
if err != nil {
t.Fatalf("err: %s", err)
}

if g.Path.String() != addrs.RootModuleInstance.String() {
t.Fatalf("wrong module path %q", g.Path)
}

// This test is here to make sure we properly detect references inside
// the special "dynamic" block construct. The most important thing here
// is that at the end test_thing.c depends on both test_thing.a and
// test_thing.b. Other details might shift over time as other logic in
// the graph builders changes.
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
meta.count-boundary (EachMode fixup)
provider.test
test_thing.a
test_thing.b
test_thing.c
provider.test
provider.test (close)
provider.test
test_thing.a
test_thing.b
test_thing.c
root
meta.count-boundary (EachMode fixup)
provider.test (close)
test_thing.a
provider.test
test_thing.b
provider.test
test_thing.c
provider.test
test_thing.a
test_thing.b
`)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}

func TestPlanGraphBuilder_targetModule(t *testing.T) {
b := &PlanGraphBuilder{
Config: testModule(t, "graph-builder-plan-target-module-provider"),
Expand Down
14 changes: 14 additions & 0 deletions terraform/test-fixtures/graph-builder-plan-dynblock/dynblock.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
resource "test_thing" "a" {
}

resource "test_thing" "b" {
}

resource "test_thing" "c" {
dynamic "nested" {
for_each = test_thing.a.list
content {
foo = test_thing.b.id
}
}
}

0 comments on commit 50a101a

Please sign in to comment.