From 6631d7cd0a68d20ba7d0aa559f883c86158fffd9 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 19 Dec 2018 17:20:50 -0800 Subject: [PATCH] ext/dynblock: Make iterator variables visible to nested block for_each Previously we were incorrectly passing down the original forEachCtx down to nested child blocks for recursive expansion. Instead, we must use the iteration-specific constructed EvalContext, which then allows any nested dynamic blocks to use the parent's iterator variable in their for_each or labels expressions, and thus unpack nested data structures into corresponding nested block structures: dynamic "parent" { for_each = [["a", "b"], []] content { dynamic "child" { for_each = parent.value content {} } } } --- ext/dynblock/expand_body.go | 3 +- ext/dynblock/expand_body_test.go | 56 ++++++++++++++++++++++++++++++++ ext/dynblock/expand_spec.go | 7 ++-- ext/dynblock/iteration.go | 10 +++--- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/ext/dynblock/expand_body.go b/ext/dynblock/expand_body.go index 697440e..97d1e4d 100644 --- a/ext/dynblock/expand_body.go +++ b/ext/dynblock/expand_body.go @@ -234,7 +234,8 @@ func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, } func (b *expandBody) expandChild(child hcl.Body, i *iteration) hcl.Body { - ret := Expand(child, b.forEachCtx) + chiCtx := i.EvalContext(b.forEachCtx) + ret := Expand(child, chiCtx) ret.(*expandBody).iteration = i return ret } diff --git a/ext/dynblock/expand_body_test.go b/ext/dynblock/expand_body_test.go index 9cc4733..b8d10e2 100644 --- a/ext/dynblock/expand_body_test.go +++ b/ext/dynblock/expand_body_test.go @@ -145,6 +145,52 @@ func TestExpand(t *testing.T) { }, }), }, + { + Type: "dynamic", + Labels: []string{"b"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ + "foo": cty.ListVal([]cty.Value{ + cty.StringVal("dynamic c nested 0"), + cty.StringVal("dynamic c nested 1"), + }), + })), + "iterator": hcltest.MockExprVariable("dyn_b"), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Blocks: hcl.Blocks{ + { + Type: "dynamic", + Labels: []string{"c"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprTraversalSrc("dyn_b.value"), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val0": hcltest.MockExprTraversalSrc("c.value"), + "val1": hcltest.MockExprTraversalSrc("dyn_b.key"), + }), + }), + }, + }, + }), + }, + }, + }), + }, + }, + }), + }, { Type: "a", Labels: []string{"static1"}, @@ -268,6 +314,16 @@ func TestExpand(t *testing.T) { "val1": cty.StringVal("dynamic b 1"), }), }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c nested 0"), + "val1": cty.StringVal("foo"), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c nested 1"), + "val1": cty.StringVal("foo"), + }), + }), }) if !got.RawEquals(want) { diff --git a/ext/dynblock/expand_spec.go b/ext/dynblock/expand_spec.go index be76cd0..41c0be2 100644 --- a/ext/dynblock/expand_spec.go +++ b/ext/dynblock/expand_spec.go @@ -41,11 +41,14 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx) diags = append(diags, eachDiags...) - if !eachVal.CanIterateElements() { + if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType { + // We skip this error for DynamicPseudoType because that means we either + // have a null (which is checked immediately below) or an unknown + // (which is handled in the expandBody Content methods). diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid dynamic for_each value", - Detail: fmt.Sprintf("Cannot use a value of type %s in for_each. An iterable collection is required.", eachVal.Type()), + Detail: fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()), Subject: eachAttr.Expr.Range().Ptr(), Expression: eachAttr.Expr, EvalContext: b.forEachCtx, diff --git a/ext/dynblock/iteration.go b/ext/dynblock/iteration.go index 580fa43..7056d33 100644 --- a/ext/dynblock/iteration.go +++ b/ext/dynblock/iteration.go @@ -30,12 +30,14 @@ func (i *iteration) Object() cty.Value { func (i *iteration) EvalContext(base *hcl.EvalContext) *hcl.EvalContext { new := base.NewChild() - new.Variables = map[string]cty.Value{} - for name, otherIt := range i.Inherited { - new.Variables[name] = otherIt.Object() + if i != nil { + new.Variables = map[string]cty.Value{} + for name, otherIt := range i.Inherited { + new.Variables[name] = otherIt.Object() + } + new.Variables[i.IteratorName] = i.Object() } - new.Variables[i.IteratorName] = i.Object() return new }