lang: Detect references when a list/set attr is defined using blocks

For compatibility with documented patterns from existing providers we are
now allowing (via a pre-processing step) any attribute whose type is a
list-of-object or set-of-object type to optionally be assigned using one
or more blocks whose type is the attribute name.

The pre-processing functionality was implemented in previous commits but
we were not correctly detecting references within these blocks that are,
from the perspective of the primary schema, invalid. Now we'll use an
alternative implementation of variable detection that is able to apply the
same schema rewriting technique we used to implement the transform and
thus can find all of the references as if they were already in their
final locations.
This commit is contained in:
Martin Atkins 2019-03-28 10:07:16 -07:00
parent 8746e9e8ad
commit 003317d7c8
4 changed files with 100 additions and 8 deletions

View File

@ -7,6 +7,9 @@ import (
) )
func ambiguousNames(schema *configschema.Block) map[string]struct{} { func ambiguousNames(schema *configschema.Block) map[string]struct{} {
if schema == nil {
return nil
}
ambiguousNames := make(map[string]struct{}) ambiguousNames := make(map[string]struct{})
for name, attrS := range schema.Attributes { for name, attrS := range schema.Attributes {
aty := attrS.Type aty := attrS.Type

View File

@ -1,10 +1,10 @@
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/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/lang/blocktoattr"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -51,14 +51,21 @@ func ReferencesInBlock(body hcl.Body, schema *configschema.Block) ([]*addrs.Refe
if body == nil { if body == nil {
return nil, nil return nil, nil
} }
spec := schema.DecoderSpec()
// We use dynblock.VariablesHCLDec instead of hcldec.Variables here because // We use blocktoattr.ExpandedVariables instead of hcldec.Variables or
// when we evaluate a block we'll apply the HCL dynamic block extension // dynblock.VariablesHCLDec here because when we evaluate a block we'll
// expansion to it first, and so we need this specialized version in order // first apply the dynamic block extension and _then_ the blocktoattr
// to properly understand what the dependencies will be once expanded. // transform, and so blocktoattr.ExpandedVariables takes into account
// Otherwise, we'd miss references that only occur inside dynamic blocks. // both of those transforms when it analyzes the body to ensure we find
traversals := dynblock.VariablesHCLDec(body, spec) // all of the references as if they'd already moved into their final
// locations, even though we can't expand dynamic blocks yet until we
// already know which variables are required.
//
// The set of cases we want to detect here is covered by the tests for
// the plan graph builder in the main 'terraform' package, since it's
// in a better position to test this due to having mock providers etc
// available.
traversals := blocktoattr.ExpandedVariables(body, schema)
return References(traversals) return References(traversals)
} }

View File

@ -152,6 +152,80 @@ test_thing.c
} }
} }
func TestPlanGraphBuilder_attrAsBlocks(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},
"nested": {
Type: cty.List(cty.Object(map[string]cty.Type{
"foo": cty.String,
})),
Optional: true,
},
},
},
},
},
}
components := &basicComponentFactory{
providers: map[string]providers.Factory{
"test": providers.FactoryFixed(provider),
},
}
b := &PlanGraphBuilder{
Config: testModule(t, "graph-builder-plan-attr-as-blocks"),
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 "nested" block that is actually defined in the schema as a
// list-of-objects attribute. This requires some special effort
// inside lang.ReferencesInBlock to make sure it searches blocks of
// type "nested" along with an attribute named "nested".
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
meta.count-boundary (EachMode fixup)
provider.test
test_thing.a
test_thing.b
provider.test
provider.test (close)
provider.test
test_thing.a
test_thing.b
root
meta.count-boundary (EachMode fixup)
provider.test (close)
test_thing.a
provider.test
test_thing.b
provider.test
test_thing.a
`)
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,8 @@
resource "test_thing" "a" {
}
resource "test_thing" "b" {
nested {
foo = test_thing.a.id
}
}