From 3dfebdfc4595eca897f66eef76d9ec026554c3dc Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 30 Apr 2019 11:11:43 -0700 Subject: [PATCH] ext/dynblock: Stub out contents when for_each is unknown Previously our behavior for an unknown for_each was to produce a single block whose content was the result of evaluating content with the iterator set to cty.DynamicVal. That produced a reasonable idea of the content, but the number of blocks in the result was still not accurate, and that can present a problem for applications that use unknown values to predict the overall shape of a not-yet-complete structure. We can't return an unknown block via the HCL API, but to make that situation easier to recognize by callers we'll now go a little further and force _all_ of the leaf attributes in such a block to be unknown values, even if they are constants in the configuration. This allows a calling application that is making predictions to use a single object whose leaves are all unknown as a heuristic to recognize what is effectively an unknown set of blocks. This is still not a perfect heuristic, but is the best we can do here within the HCL API assumptions. A fundamental assumption of the HCL API is that it's possible to walk the block structure without evaluating any expressions and the dynamic block extension is intentionally subverting that assumption, so some oddities are to be expected. Calling applications that need a fully reliable sense of the final structure should not use the dynamic block extension. --- ext/dynblock/expand_body.go | 10 ++++ ext/dynblock/expand_body_test.go | 53 +++++++++++++++++++- ext/dynblock/unknown_body.go | 84 ++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 ext/dynblock/unknown_body.go 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 +}