diff --git a/ext/dynblock/expand_body.go b/ext/dynblock/expand_body.go index 97d1e4d..dd30822 100644 --- a/ext/dynblock/expand_body.go +++ b/ext/dynblock/expand_body.go @@ -213,6 +213,16 @@ func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, diags = append(diags, blockDiags...) if block != nil { block.Body = b.expandChild(block.Body, i) + + // We additionally force all of the leaf attribute values + // in the result to be unknown so the calling application + // can, if necessary, use that as a heuristic to detect + // when a single nested block might be standing in for + // multiple blocks yet to be expanded. This retains the + // structure of the generated body but forces all of its + // leaf attribute values to be unknown. + block.Body = unknownBody{block.Body} + blocks = append(blocks, block) } } diff --git a/ext/dynblock/expand_body_test.go b/ext/dynblock/expand_body_test.go index b8d10e2..350dbe3 100644 --- a/ext/dynblock/expand_body_test.go +++ b/ext/dynblock/expand_body_test.go @@ -3,9 +3,8 @@ package dynblock import ( "testing" - "github.com/hashicorp/hcl2/hcldec" - "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcldec" "github.com/hashicorp/hcl2/hcltest" "github.com/zclconf/go-cty/cty" ) @@ -191,6 +190,47 @@ 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.UnknownVal(cty.Map(cty.String))), + "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"}, @@ -324,6 +364,15 @@ func TestExpand(t *testing.T) { "val1": cty.StringVal("foo"), }), }), + cty.ListVal([]cty.Value{ + // This one comes from a dynamic block with an unknown for_each + // value, so we produce a single block object with all of the + // leaf attribute values set to unknown values. + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.UnknownVal(cty.String), + "val1": cty.UnknownVal(cty.String), + }), + }), }) if !got.RawEquals(want) { diff --git a/ext/dynblock/unknown_body.go b/ext/dynblock/unknown_body.go new file mode 100644 index 0000000..932f6a3 --- /dev/null +++ b/ext/dynblock/unknown_body.go @@ -0,0 +1,84 @@ +package dynblock + +import ( + "github.com/hashicorp/hcl2/hcl" + "github.com/zclconf/go-cty/cty" +) + +// unknownBody is a funny body that just reports everything inside it as +// unknown. It uses a given other body as a sort of template for what attributes +// and blocks are inside -- including source location information -- but +// subsitutes unknown values of unknown type for all attributes. +// +// This rather odd process is used to handle expansion of dynamic blocks whose +// for_each expression is unknown. Since a block cannot itself be unknown, +// we instead arrange for everything _inside_ the block to be unknown instead, +// to give the best possible approximation. +type unknownBody struct { + template hcl.Body +} + +var _ hcl.Body = unknownBody{} + +func (b unknownBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { + content, diags := b.template.Content(schema) + content = b.fixupContent(content) + + // We're intentionally preserving the diagnostics reported from the + // inner body so that we can still report where the template body doesn't + // match the requested schema. + return content, diags +} + +func (b unknownBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { + content, remain, diags := b.template.PartialContent(schema) + content = b.fixupContent(content) + remain = unknownBody{remain} // remaining content must also be wrapped + + // We're intentionally preserving the diagnostics reported from the + // inner body so that we can still report where the template body doesn't + // match the requested schema. + return content, remain, diags +} + +func (b unknownBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { + attrs, diags := b.template.JustAttributes() + attrs = b.fixupAttrs(attrs) + + // We're intentionally preserving the diagnostics reported from the + // inner body so that we can still report where the template body doesn't + // match the requested schema. + return attrs, diags +} + +func (b unknownBody) MissingItemRange() hcl.Range { + return b.template.MissingItemRange() +} + +func (b unknownBody) fixupContent(got *hcl.BodyContent) *hcl.BodyContent { + ret := &hcl.BodyContent{} + ret.Attributes = b.fixupAttrs(got.Attributes) + if len(got.Blocks) > 0 { + ret.Blocks = make(hcl.Blocks, 0, len(got.Blocks)) + for _, gotBlock := range got.Blocks { + new := *gotBlock // shallow copy + new.Body = unknownBody{gotBlock.Body} // nested content must also be marked unknown + ret.Blocks = append(ret.Blocks, &new) + } + } + + return ret +} + +func (b unknownBody) fixupAttrs(got hcl.Attributes) hcl.Attributes { + if len(got) == 0 { + return nil + } + ret := make(hcl.Attributes, len(got)) + for name, gotAttr := range got { + new := *gotAttr // shallow copy + new.Expr = hcl.StaticExpr(cty.DynamicVal, gotAttr.Expr.Range()) + ret[name] = &new + } + return ret +}