lang: Consider "dynamic" blocks when resolving references

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.
This commit is contained in:
Martin Atkins 2019-03-18 17:15:23 -07:00
parent 838a42d218
commit 50a101afbd
3 changed files with 107 additions and 2 deletions

View File

@ -1,8 +1,8 @@
package lang package lang
import ( import (
"github.com/hashicorp/hcl2/ext/dynblock"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcldec"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
@ -52,7 +52,13 @@ func ReferencesInBlock(body hcl.Body, schema *configschema.Block) ([]*addrs.Refe
return nil, nil return nil, nil
} }
spec := schema.DecoderSpec() 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) return References(traversals)
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/providers"
"github.com/zclconf/go-cty/cty"
) )
func TestPlanGraphBuilder_impl(t *testing.T) { func TestPlanGraphBuilder_impl(t *testing.T) {
@ -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) { func TestPlanGraphBuilder_targetModule(t *testing.T) {
b := &PlanGraphBuilder{ b := &PlanGraphBuilder{
Config: testModule(t, "graph-builder-plan-target-module-provider"), Config: testModule(t, "graph-builder-plan-target-module-provider"),

View File

@ -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
}
}
}