summaryrefslogtreecommitdiff
path: root/ext/dynblock
diff options
context:
space:
mode:
authorMartin Atkins <mart@degeneration.co.uk>2018-01-21 15:29:43 -0800
committerMartin Atkins <mart@degeneration.co.uk>2018-01-27 09:10:18 -0800
commitda95646a33954d8ea196ece76e32d9684dff6ac6 (patch)
tree69bb94a712adc16679d05298f8d1cb58340ef556 /ext/dynblock
parentf87600a7d93ee66499e0566bc9ce0bb43e91a28a (diff)
ext/dynblock: dynamic blocks extension
This extension allows an application to support dynamic generation of child blocks based on expressions in certain contexts. This is done using a new block type called "dynamic", which contains an iteration value (which must be a collection) and a specification of how to construct a child block for each element of that collection.
Diffstat (limited to 'ext/dynblock')
-rw-r--r--ext/dynblock/README.md99
-rw-r--r--ext/dynblock/expand_body.go251
-rw-r--r--ext/dynblock/expand_body_test.go278
-rw-r--r--ext/dynblock/expand_spec.go202
-rw-r--r--ext/dynblock/expr_wrap.go60
-rw-r--r--ext/dynblock/iteration.go64
-rw-r--r--ext/dynblock/public.go44
-rw-r--r--ext/dynblock/schema.go50
-rw-r--r--ext/dynblock/variables.go72
9 files changed, 1120 insertions, 0 deletions
diff --git a/ext/dynblock/README.md b/ext/dynblock/README.md
new file mode 100644
index 0000000..4c42e6d
--- /dev/null
+++ b/ext/dynblock/README.md
@@ -0,0 +1,99 @@
+# HCL Dynamic Blocks Extension
+
+This HCL extension implements a special block type named "dynamic" that can
+be used to dynamically generate blocks of other types by iterating over
+collection values.
+
+Normally the block structure in an HCL configuration file is rigid, even
+though dynamic expressions can be used within attribute values. This is
+convenient for most applications since it allows the overall structure of
+the document to be decoded easily, but in some applications it is desirable
+to allow dynamic block generation within certain portions of the configuration.
+
+Dynamic block generation is performed using the `dynamic` block type:
+
+```hcl
+toplevel {
+ nested {
+ foo = "static block 1"
+ }
+
+ dynamic "nested" {
+ for_each = ["a", "b", "c"]
+ iterator = nested
+ content {
+ foo = "dynamic block ${nested.value}"
+ }
+ }
+
+ nested {
+ foo = "static block 2"
+ }
+}
+```
+
+The above is interpreted as if it were written as follows:
+
+```hcl
+toplevel {
+ nested {
+ foo = "static block 1"
+ }
+
+ nested {
+ foo = "dynamic block a"
+ }
+
+ nested {
+ foo = "dynamic block b"
+ }
+
+ nested {
+ foo = "dynamic block c"
+ }
+
+ nested {
+ foo = "static block 2"
+ }
+}
+```
+
+Since HCL block syntax is not normally exposed to the possibility of unknown
+values, this extension must make some compromises when asked to iterate over
+an unknown collection. If the length of the collection cannot be statically
+recognized (because it is an unknown value of list, map, or set type) then
+the `dynamic` construct will generate a _single_ dynamic block whose iterator
+key and value are both unknown values of the dynamic pseudo-type, thus causing
+any attribute values derived from iteration to appear as unknown values. There
+is no explicit representation of the fact that the length of the collection may
+eventually be different than one.
+
+## Usage
+
+Pass a body to function `Expand` to obtain a new body that will, on access
+to its content, evaluate and expand any nested `dynamic` blocks.
+Dynamic block processing is also automatically propagated into any nested
+blocks that are returned, allowing users to nest dynamic blocks inside
+one another and to nest dynamic blocks inside other static blocks.
+
+HCL structural decoding does not normally have access to an `EvalContext`, so
+any variables and functions that should be available to the `for_each`
+and `labels` expressions must be passed in when calling `Expand`. Expressions
+within the `content` block are evaluated separately and so can be passed a
+separate `EvalContext` if desired, during normal attribute expression
+evaluation.
+
+Some applications dynamically generate an `EvalContext` by analyzing which
+variables are referenced by an expression before evaluating it. This can be
+achieved for a block that might contain `dynamic` blocks by calling
+`ForEachVariables`, which returns the variables required by the `for_each`
+and `labels` attributes in all `dynamic` blocks within the given body,
+including any nested `dynamic` blocks.
+
+# Performance
+
+This extension is going quite harshly against the grain of the HCL API, and
+so it uses lots of wrapping objects and temporary data structures to get its
+work done. HCL in general is not suitable for use in high-performance situations
+or situations sensitive to memory pressure, but that is _especially_ true for
+this extension.
diff --git a/ext/dynblock/expand_body.go b/ext/dynblock/expand_body.go
new file mode 100644
index 0000000..697440e
--- /dev/null
+++ b/ext/dynblock/expand_body.go
@@ -0,0 +1,251 @@
+package dynblock
+
+import (
+ "fmt"
+
+ "github.com/hashicorp/hcl2/hcl"
+ "github.com/zclconf/go-cty/cty"
+)
+
+// expandBody wraps another hcl.Body and expands any "dynamic" blocks found
+// inside whenever Content or PartialContent is called.
+type expandBody struct {
+ original hcl.Body
+ forEachCtx *hcl.EvalContext
+ iteration *iteration // non-nil if we're nested inside another "dynamic" block
+
+ // These are used with PartialContent to produce a "remaining items"
+ // body to return. They are nil on all bodies fresh out of the transformer.
+ //
+ // Note that this is re-implemented here rather than delegating to the
+ // existing support required by the underlying body because we need to
+ // retain access to the entire original body on subsequent decode operations
+ // so we can retain any "dynamic" blocks for types we didn't take consume
+ // on the first pass.
+ hiddenAttrs map[string]struct{}
+ hiddenBlocks map[string]hcl.BlockHeaderSchema
+}
+
+func (b *expandBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
+ extSchema := b.extendSchema(schema)
+ rawContent, diags := b.original.Content(extSchema)
+
+ blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, false)
+ diags = append(diags, blockDiags...)
+ attrs := b.prepareAttributes(rawContent.Attributes)
+
+ content := &hcl.BodyContent{
+ Attributes: attrs,
+ Blocks: blocks,
+ MissingItemRange: b.original.MissingItemRange(),
+ }
+
+ return content, diags
+}
+
+func (b *expandBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
+ extSchema := b.extendSchema(schema)
+ rawContent, _, diags := b.original.PartialContent(extSchema)
+ // We discard the "remain" argument above because we're going to construct
+ // our own remain that also takes into account remaining "dynamic" blocks.
+
+ blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, true)
+ diags = append(diags, blockDiags...)
+ attrs := b.prepareAttributes(rawContent.Attributes)
+
+ content := &hcl.BodyContent{
+ Attributes: attrs,
+ Blocks: blocks,
+ MissingItemRange: b.original.MissingItemRange(),
+ }
+
+ remain := &expandBody{
+ original: b.original,
+ forEachCtx: b.forEachCtx,
+ iteration: b.iteration,
+ hiddenAttrs: make(map[string]struct{}),
+ hiddenBlocks: make(map[string]hcl.BlockHeaderSchema),
+ }
+ for name := range b.hiddenAttrs {
+ remain.hiddenAttrs[name] = struct{}{}
+ }
+ for typeName, blockS := range b.hiddenBlocks {
+ remain.hiddenBlocks[typeName] = blockS
+ }
+ for _, attrS := range schema.Attributes {
+ remain.hiddenAttrs[attrS.Name] = struct{}{}
+ }
+ for _, blockS := range schema.Blocks {
+ remain.hiddenBlocks[blockS.Type] = blockS
+ }
+
+ return content, remain, diags
+}
+
+func (b *expandBody) extendSchema(schema *hcl.BodySchema) *hcl.BodySchema {
+ // We augment the requested schema to also include our special "dynamic"
+ // block type, since then we'll get instances of it interleaved with
+ // all of the literal child blocks we must also include.
+ extSchema := &hcl.BodySchema{
+ Attributes: schema.Attributes,
+ Blocks: make([]hcl.BlockHeaderSchema, len(schema.Blocks), len(schema.Blocks)+len(b.hiddenBlocks)+1),
+ }
+ copy(extSchema.Blocks, schema.Blocks)
+ extSchema.Blocks = append(extSchema.Blocks, dynamicBlockHeaderSchema)
+
+ // If we have any hiddenBlocks then we also need to register those here
+ // so that a call to "Content" on the underlying body won't fail.
+ // (We'll filter these out again once we process the result of either
+ // Content or PartialContent.)
+ for _, blockS := range b.hiddenBlocks {
+ extSchema.Blocks = append(extSchema.Blocks, blockS)
+ }
+
+ // If we have any hiddenAttrs then we also need to register these, for
+ // the same reason as we deal with hiddenBlocks above.
+ if len(b.hiddenAttrs) != 0 {
+ newAttrs := make([]hcl.AttributeSchema, len(schema.Attributes), len(schema.Attributes)+len(b.hiddenAttrs))
+ copy(newAttrs, extSchema.Attributes)
+ for name := range b.hiddenAttrs {
+ newAttrs = append(newAttrs, hcl.AttributeSchema{
+ Name: name,
+ Required: false,
+ })
+ }
+ extSchema.Attributes = newAttrs
+ }
+
+ return extSchema
+}
+
+func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) hcl.Attributes {
+ if len(b.hiddenAttrs) == 0 && b.iteration == nil {
+ // Easy path: just pass through the attrs from the original body verbatim
+ return rawAttrs
+ }
+
+ // Otherwise we have some work to do: we must filter out any attributes
+ // that are hidden (since a previous PartialContent call already saw these)
+ // and wrap the expressions of the inner attributes so that they will
+ // have access to our iteration variables.
+ attrs := make(hcl.Attributes, len(rawAttrs))
+ for name, rawAttr := range rawAttrs {
+ if _, hidden := b.hiddenAttrs[name]; hidden {
+ continue
+ }
+ if b.iteration != nil {
+ attr := *rawAttr // shallow copy so we can mutate it
+ attr.Expr = exprWrap{
+ Expression: attr.Expr,
+ i: b.iteration,
+ }
+ attrs[name] = &attr
+ } else {
+ // If we have no active iteration then no wrapping is required.
+ attrs[name] = rawAttr
+ }
+ }
+ return attrs
+}
+
+func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, partial bool) (hcl.Blocks, hcl.Diagnostics) {
+ var blocks hcl.Blocks
+ var diags hcl.Diagnostics
+
+ for _, rawBlock := range rawBlocks {
+ switch rawBlock.Type {
+ case "dynamic":
+ realBlockType := rawBlock.Labels[0]
+ if _, hidden := b.hiddenBlocks[realBlockType]; hidden {
+ continue
+ }
+
+ var blockS *hcl.BlockHeaderSchema
+ for _, candidate := range schema.Blocks {
+ if candidate.Type == realBlockType {
+ blockS = &candidate
+ break
+ }
+ }
+ if blockS == nil {
+ // Not a block type that the caller requested.
+ if !partial {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Unsupported block type",
+ Detail: fmt.Sprintf("Blocks of type %q are not expected here.", realBlockType),
+ Subject: &rawBlock.LabelRanges[0],
+ })
+ }
+ continue
+ }
+
+ spec, specDiags := b.decodeSpec(blockS, rawBlock)
+ diags = append(diags, specDiags...)
+ if specDiags.HasErrors() {
+ continue
+ }
+
+ if spec.forEachVal.IsKnown() {
+ for it := spec.forEachVal.ElementIterator(); it.Next(); {
+ key, value := it.Element()
+ i := b.iteration.MakeChild(spec.iteratorName, key, value)
+
+ block, blockDiags := spec.newBlock(i, b.forEachCtx)
+ diags = append(diags, blockDiags...)
+ if block != nil {
+ // Attach our new iteration context so that attributes
+ // and other nested blocks can refer to our iterator.
+ block.Body = b.expandChild(block.Body, i)
+ blocks = append(blocks, block)
+ }
+ }
+ } else {
+ // If our top-level iteration value isn't known then we're forced
+ // to compromise since HCL doesn't have any concept of an
+ // "unknown block". In this case then, we'll produce a single
+ // dynamic block with the iterator values set to DynamicVal,
+ // which at least makes the potential for a block visible
+ // in our result, even though it's not represented in a fully-accurate
+ // way.
+ i := b.iteration.MakeChild(spec.iteratorName, cty.DynamicVal, cty.DynamicVal)
+ block, blockDiags := spec.newBlock(i, b.forEachCtx)
+ diags = append(diags, blockDiags...)
+ if block != nil {
+ block.Body = b.expandChild(block.Body, i)
+ blocks = append(blocks, block)
+ }
+ }
+
+ default:
+ if _, hidden := b.hiddenBlocks[rawBlock.Type]; !hidden {
+ // A static block doesn't create a new iteration context, but
+ // it does need to inherit _our own_ iteration context in
+ // case it contains expressions that refer to our inherited
+ // iterators, or nested "dynamic" blocks.
+ expandedBlock := *rawBlock // shallow copy
+ expandedBlock.Body = b.expandChild(rawBlock.Body, b.iteration)
+ blocks = append(blocks, &expandedBlock)
+ }
+ }
+ }
+
+ return blocks, diags
+}
+
+func (b *expandBody) expandChild(child hcl.Body, i *iteration) hcl.Body {
+ ret := Expand(child, b.forEachCtx)
+ ret.(*expandBody).iteration = i
+ return ret
+}
+
+func (b *expandBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
+ // blocks aren't allowed in JustAttributes mode and this body can
+ // only produce blocks, so we'll just pass straight through to our
+ // underlying body here.
+ return b.original.JustAttributes()
+}
+
+func (b *expandBody) MissingItemRange() hcl.Range {
+ return b.original.MissingItemRange()
+}
diff --git a/ext/dynblock/expand_body_test.go b/ext/dynblock/expand_body_test.go
new file mode 100644
index 0000000..9cc4733
--- /dev/null
+++ b/ext/dynblock/expand_body_test.go
@@ -0,0 +1,278 @@
+package dynblock
+
+import (
+ "testing"
+
+ "github.com/hashicorp/hcl2/hcldec"
+
+ "github.com/hashicorp/hcl2/hcl"
+ "github.com/hashicorp/hcl2/hcltest"
+ "github.com/zclconf/go-cty/cty"
+)
+
+func TestExpand(t *testing.T) {
+ srcBody := hcltest.MockBody(&hcl.BodyContent{
+ Blocks: hcl.Blocks{
+ {
+ Type: "a",
+ Labels: []string{"static0"},
+ LabelRanges: []hcl.Range{hcl.Range{}},
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
+ "val": hcltest.MockExprLiteral(cty.StringVal("static a 0")),
+ }),
+ }),
+ },
+ {
+ Type: "b",
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Blocks: hcl.Blocks{
+ {
+ Type: "c",
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
+ "val0": hcltest.MockExprLiteral(cty.StringVal("static c 0")),
+ }),
+ }),
+ },
+ {
+ 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.MockExprLiteral(cty.ListVal([]cty.Value{
+ cty.StringVal("dynamic c 0"),
+ cty.StringVal("dynamic c 1"),
+ })),
+ "iterator": hcltest.MockExprVariable("dyn_c"),
+ }),
+ Blocks: hcl.Blocks{
+ {
+ Type: "content",
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
+ "val0": hcltest.MockExprTraversalSrc("dyn_c.value"),
+ }),
+ }),
+ },
+ },
+ }),
+ },
+ },
+ }),
+ },
+ {
+ Type: "dynamic",
+ Labels: []string{"a"},
+ LabelRanges: []hcl.Range{hcl.Range{}},
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
+ "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
+ cty.StringVal("dynamic a 0"),
+ cty.StringVal("dynamic a 1"),
+ cty.StringVal("dynamic a 2"),
+ })),
+ "labels": hcltest.MockExprList([]hcl.Expression{
+ hcltest.MockExprTraversalSrc("a.key"),
+ }),
+ }),
+ Blocks: hcl.Blocks{
+ {
+ Type: "content",
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
+ "val": hcltest.MockExprTraversalSrc("a.value"),
+ }),
+ }),
+ },
+ },
+ }),
+ },
+ {
+ 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.ListVal([]cty.Value{
+ cty.StringVal("dynamic b 0"),
+ cty.StringVal("dynamic b 1"),
+ })),
+ "iterator": hcltest.MockExprVariable("dyn_b"),
+ }),
+ Blocks: hcl.Blocks{
+ {
+ Type: "content",
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Blocks: hcl.Blocks{
+ {
+ Type: "c",
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
+ "val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")),
+ "val1": hcltest.MockExprTraversalSrc("dyn_b.value"),
+ }),
+ }),
+ },
+ {
+ 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.MockExprLiteral(cty.ListVal([]cty.Value{
+ cty.StringVal("dynamic c 2"),
+ cty.StringVal("dynamic c 3"),
+ })),
+ }),
+ 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.value"),
+ }),
+ }),
+ },
+ },
+ }),
+ },
+ },
+ }),
+ },
+ },
+ }),
+ },
+ {
+ Type: "a",
+ Labels: []string{"static1"},
+ LabelRanges: []hcl.Range{hcl.Range{}},
+ Body: hcltest.MockBody(&hcl.BodyContent{
+ Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
+ "val": hcltest.MockExprLiteral(cty.StringVal("static a 1")),
+ }),
+ }),
+ },
+ },
+ })
+
+ dynBody := Expand(srcBody, nil)
+ var remain hcl.Body
+
+ t.Run("PartialDecode", func(t *testing.T) {
+ decSpec := &hcldec.BlockMapSpec{
+ TypeName: "a",
+ LabelNames: []string{"key"},
+ Nested: &hcldec.AttrSpec{
+ Name: "val",
+ Type: cty.String,
+ Required: true,
+ },
+ }
+
+ var got cty.Value
+ var diags hcl.Diagnostics
+ got, remain, diags = hcldec.PartialDecode(dynBody, decSpec, nil)
+ if len(diags) != 0 {
+ t.Errorf("unexpected diagnostics")
+ for _, diag := range diags {
+ t.Logf("- %s", diag)
+ }
+ return
+ }
+
+ want := cty.MapVal(map[string]cty.Value{
+ "static0": cty.StringVal("static a 0"),
+ "static1": cty.StringVal("static a 1"),
+ "0": cty.StringVal("dynamic a 0"),
+ "1": cty.StringVal("dynamic a 1"),
+ "2": cty.StringVal("dynamic a 2"),
+ })
+
+ if !got.RawEquals(want) {
+ t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
+ }
+ })
+
+ t.Run("Decode", func(t *testing.T) {
+ decSpec := &hcldec.BlockListSpec{
+ TypeName: "b",
+ Nested: &hcldec.BlockListSpec{
+ TypeName: "c",
+ Nested: &hcldec.ObjectSpec{
+ "val0": &hcldec.AttrSpec{
+ Name: "val0",
+ Type: cty.String,
+ },
+ "val1": &hcldec.AttrSpec{
+ Name: "val1",
+ Type: cty.String,
+ },
+ },
+ },
+ }
+
+ var got cty.Value
+ var diags hcl.Diagnostics
+ got, diags = hcldec.Decode(remain, decSpec, nil)
+ if len(diags) != 0 {
+ t.Errorf("unexpected diagnostics")
+ for _, diag := range diags {
+ t.Logf("- %s", diag)
+ }
+ return
+ }
+
+ want := cty.ListVal([]cty.Value{
+ cty.ListVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("static c 0"),
+ "val1": cty.NullVal(cty.String),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("dynamic c 0"),
+ "val1": cty.NullVal(cty.String),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("dynamic c 1"),
+ "val1": cty.NullVal(cty.String),
+ }),
+ }),
+ cty.ListVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("static c 1"),
+ "val1": cty.StringVal("dynamic b 0"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("dynamic c 2"),
+ "val1": cty.StringVal("dynamic b 0"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("dynamic c 3"),
+ "val1": cty.StringVal("dynamic b 0"),
+ }),
+ }),
+ cty.ListVal([]cty.Value{
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("static c 1"),
+ "val1": cty.StringVal("dynamic b 1"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("dynamic c 2"),
+ "val1": cty.StringVal("dynamic b 1"),
+ }),
+ cty.ObjectVal(map[string]cty.Value{
+ "val0": cty.StringVal("dynamic c 3"),
+ "val1": cty.StringVal("dynamic b 1"),
+ }),
+ }),
+ })
+
+ if !got.RawEquals(want) {
+ t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
+ }
+ })
+
+}
diff --git a/ext/dynblock/expand_spec.go b/ext/dynblock/expand_spec.go
new file mode 100644
index 0000000..fa91c9f
--- /dev/null
+++ b/ext/dynblock/expand_spec.go
@@ -0,0 +1,202 @@
+package dynblock
+
+import (
+ "fmt"
+
+ "github.com/hashicorp/hcl2/hcl"
+ "github.com/zclconf/go-cty/cty"
+ "github.com/zclconf/go-cty/cty/convert"
+)
+
+type expandSpec struct {
+ blockType string
+ blockTypeRange hcl.Range
+ defRange hcl.Range
+ forEachVal cty.Value
+ iteratorName string
+ labelExprs []hcl.Expression
+ contentBody hcl.Body
+ inherited map[string]*iteration
+}
+
+func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandSpec, hcl.Diagnostics) {
+ var diags hcl.Diagnostics
+
+ var schema *hcl.BodySchema
+ if len(blockS.LabelNames) != 0 {
+ schema = dynamicBlockBodySchemaLabels
+ } else {
+ schema = dynamicBlockBodySchemaNoLabels
+ }
+
+ specContent, specDiags := rawSpec.Body.Content(schema)
+ diags = append(diags, specDiags...)
+ if specDiags.HasErrors() {
+ return nil, diags
+ }
+
+ //// for_each attribute
+
+ eachAttr := specContent.Attributes["for_each"]
+ eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx)
+ diags = append(diags, eachDiags...)
+
+ if !eachVal.CanIterateElements() {
+ 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()),
+ Subject: eachAttr.Expr.Range().Ptr(),
+ })
+ return nil, diags
+ }
+ if eachVal.IsNull() {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Invalid dynamic for_each value",
+ Detail: "Cannot use a null value in for_each.",
+ Subject: eachAttr.Expr.Range().Ptr(),
+ })
+ return nil, diags
+ }
+
+ //// iterator attribute
+
+ iteratorName := blockS.Type
+ if iteratorAttr := specContent.Attributes["iterator"]; iteratorAttr != nil {
+ itTraversal, itDiags := hcl.AbsTraversalForExpr(iteratorAttr.Expr)
+ diags = append(diags, itDiags...)
+ if itDiags.HasErrors() {
+ return nil, diags
+ }
+
+ if len(itTraversal) != 1 {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Invalid dynamic iterator name",
+ Detail: "Dynamic iterator must be a single variable name.",
+ Subject: itTraversal.SourceRange().Ptr(),
+ })
+ return nil, diags
+ }
+
+ iteratorName = itTraversal.RootName()
+ }
+
+ var labelExprs []hcl.Expression
+ if labelsAttr := specContent.Attributes["labels"]; labelsAttr != nil {
+ var labelDiags hcl.Diagnostics
+ labelExprs, labelDiags = hcl.ExprList(labelsAttr.Expr)
+ diags = append(diags, labelDiags...)
+ if labelDiags.HasErrors() {
+ return nil, diags
+ }
+
+ if len(labelExprs) > len(blockS.LabelNames) {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Extraneous dynamic block label",
+ Detail: fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
+ Subject: labelExprs[len(blockS.LabelNames)].Range().Ptr(),
+ })
+ return nil, diags
+ } else if len(labelExprs) < len(blockS.LabelNames) {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Insufficient dynamic block labels",
+ Detail: fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)),
+ Subject: labelsAttr.Expr.Range().Ptr(),
+ })
+ return nil, diags
+ }
+ }
+
+ // Since our schema requests only blocks of type "content", we can assume
+ // that all entries in specContent.Blocks are content blocks.
+ if len(specContent.Blocks) == 0 {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Missing dynamic content block",
+ Detail: "A dynamic block must have a nested block of type \"content\" to describe the body of each generated block.",
+ Subject: &specContent.MissingItemRange,
+ })
+ return nil, diags
+ }
+ if len(specContent.Blocks) > 1 {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Extraneous dynamic content block",
+ Detail: "Only one nested content block is allowed for each dynamic block.",
+ Subject: &specContent.Blocks[1].DefRange,
+ })
+ return nil, diags
+ }
+
+ return &expandSpec{
+ blockType: blockS.Type,
+ blockTypeRange: rawSpec.LabelRanges[0],
+ defRange: rawSpec.DefRange,
+ forEachVal: eachVal,
+ iteratorName: iteratorName,
+ labelExprs: labelExprs,
+ contentBody: specContent.Blocks[0].Body,
+ }, diags
+}
+
+func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) {
+ var diags hcl.Diagnostics
+ var labels []string
+ var labelRanges []hcl.Range
+ lCtx := i.EvalContext(ctx)
+ for _, labelExpr := range s.labelExprs {
+ labelVal, labelDiags := labelExpr.Value(lCtx)
+ diags = append(diags, labelDiags...)
+ if labelDiags.HasErrors() {
+ return nil, diags
+ }
+
+ var convErr error
+ labelVal, convErr = convert.Convert(labelVal, cty.String)
+ if convErr != nil {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Invalid dynamic block label",
+ Detail: fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr),
+ Subject: labelExpr.Range().Ptr(),
+ })
+ return nil, diags
+ }
+ if labelVal.IsNull() {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Invalid dynamic block label",
+ Detail: "Cannot use a null value as a dynamic block label.",
+ Subject: labelExpr.Range().Ptr(),
+ })
+ return nil, diags
+ }
+ if !labelVal.IsKnown() {
+ diags = append(diags, &hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Invalid dynamic block label",
+ Detail: "This value is not yet known. Dynamic block labels must be immediately-known values.",
+ Subject: labelExpr.Range().Ptr(),
+ })
+ return nil, diags
+ }
+
+ labels = append(labels, labelVal.AsString())
+ labelRanges = append(labelRanges, labelExpr.Range())
+ }
+
+ block := &hcl.Block{
+ Type: s.blockType,
+ TypeRange: s.blockTypeRange,
+ Labels: labels,
+ LabelRanges: labelRanges,
+ DefRange: s.defRange,
+ Body: s.contentBody,
+ }
+
+ return block, diags
+}
diff --git a/ext/dynblock/expr_wrap.go b/ext/dynblock/expr_wrap.go
new file mode 100644
index 0000000..f29d5a6
--- /dev/null
+++ b/ext/dynblock/expr_wrap.go
@@ -0,0 +1,60 @@
+package dynblock
+
+import (
+ "github.com/hashicorp/hcl2/hcl"
+ "github.com/zclconf/go-cty/cty"
+)
+
+type exprWrap struct {
+ hcl.Expression
+ i *iteration
+}
+
+func (e exprWrap) Variables() []hcl.Traversal {
+ raw := e.Expression.Variables()
+ ret := make([]hcl.Traversal, 0, len(raw))
+
+ // Filter out traversals that refer to our iterator name or any
+ // iterator we've inherited; we're going to provide those in
+ // our Value wrapper, so the caller doesn't need to know about them.
+ for _, traversal := range raw {
+ rootName := traversal.RootName()
+ if rootName == e.i.IteratorName {
+ continue
+ }
+ if _, inherited := e.i.Inherited[rootName]; inherited {
+ continue
+ }
+ ret = append(ret, traversal)
+ }
+ return ret
+}
+
+func (e exprWrap) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
+ extCtx := e.i.EvalContext(ctx)
+ return e.Expression.Value(extCtx)
+}
+
+// Passthrough implementation for hcl.ExprList
+func (e exprWrap) ExprList() []hcl.Expression {
+ type exprList interface {
+ ExprList() []hcl.Expression
+ }
+
+ if el, supported := e.Expression.(exprList); supported {
+ return el.ExprList()
+ }
+ return nil
+}
+
+// Passthrough implementation for hcl.AbsTraversalForExpr and hcl.RelTraversalForExpr
+func (e exprWrap) AsTraversal() hcl.Traversal {
+ type asTraversal interface {
+ AsTraversal() hcl.Traversal
+ }
+
+ if at, supported := e.Expression.(asTraversal); supported {
+ return at.AsTraversal()
+ }
+ return nil
+}
diff --git a/ext/dynblock/iteration.go b/ext/dynblock/iteration.go
new file mode 100644
index 0000000..580fa43
--- /dev/null
+++ b/ext/dynblock/iteration.go
@@ -0,0 +1,64 @@
+package dynblock
+
+import (
+ "github.com/hashicorp/hcl2/hcl"
+ "github.com/zclconf/go-cty/cty"
+)
+
+type iteration struct {
+ IteratorName string
+ Key cty.Value
+ Value cty.Value
+ Inherited map[string]*iteration
+}
+
+func (s *expandSpec) MakeIteration(key, value cty.Value) *iteration {
+ return &iteration{
+ IteratorName: s.iteratorName,
+ Key: key,
+ Value: value,
+ Inherited: s.inherited,
+ }
+}
+
+func (i *iteration) Object() cty.Value {
+ return cty.ObjectVal(map[string]cty.Value{
+ "key": i.Key,
+ "value": i.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()
+ }
+ new.Variables[i.IteratorName] = i.Object()
+
+ return new
+}
+
+func (i *iteration) MakeChild(iteratorName string, key, value cty.Value) *iteration {
+ if i == nil {
+ // Create entirely new root iteration, then
+ return &iteration{
+ IteratorName: iteratorName,
+ Key: key,
+ Value: value,
+ }
+ }
+
+ inherited := map[string]*iteration{}
+ for name, otherIt := range i.Inherited {
+ inherited[name] = otherIt
+ }
+ inherited[i.IteratorName] = i
+ return &iteration{
+ IteratorName: iteratorName,
+ Key: key,
+ Value: value,
+ Inherited: inherited,
+ }
+}
diff --git a/ext/dynblock/public.go b/ext/dynblock/public.go
new file mode 100644
index 0000000..b7e8ca9
--- /dev/null
+++ b/ext/dynblock/public.go
@@ -0,0 +1,44 @@
+package dynblock
+
+import (
+ "github.com/hashicorp/hcl2/hcl"
+)
+
+// Expand "dynamic" blocks in the given body, returning a new body that
+// has those blocks expanded.
+//
+// The given EvalContext is used when evaluating "for_each" and "labels"
+// attributes within dynamic blocks, allowing those expressions access to
+// variables and functions beyond the iterator variable created by the
+// iteration.
+//
+// Expand returns no diagnostics because no blocks are actually expanded
+// until a call to Content or PartialContent on the returned body, which
+// will then expand only the blocks selected by the schema.
+//
+// "dynamic" blocks are also expanded automatically within nested blocks
+// in the given body, including within other dynamic blocks, thus allowing
+// multi-dimensional iteration. However, it is not possible to
+// dynamically-generate the "dynamic" blocks themselves except through nesting.
+//
+// parent {
+// dynamic "child" {
+// for_each = child_objs
+// content {
+// dynamic "grandchild" {
+// for_each = child.value.children
+// labels = [grandchild.key]
+// content {
+// parent_key = child.key
+// value = grandchild.value
+// }
+// }
+// }
+// }
+// }
+func Expand(body hcl.Body, ctx *hcl.EvalContext) hcl.Body {
+ return &expandBody{
+ original: body,
+ forEachCtx: ctx,
+ }
+}
diff --git a/ext/dynblock/schema.go b/ext/dynblock/schema.go
new file mode 100644
index 0000000..dc8ed5a
--- /dev/null
+++ b/ext/dynblock/schema.go
@@ -0,0 +1,50 @@
+package dynblock
+
+import "github.com/hashicorp/hcl2/hcl"
+
+var dynamicBlockHeaderSchema = hcl.BlockHeaderSchema{
+ Type: "dynamic",
+ LabelNames: []string{"type"},
+}
+
+var dynamicBlockBodySchemaLabels = &hcl.BodySchema{
+ Attributes: []hcl.AttributeSchema{
+ {
+ Name: "for_each",
+ Required: true,
+ },
+ {
+ Name: "iterator",
+ Required: false,
+ },
+ {
+ Name: "labels",
+ Required: true,
+ },
+ },
+ Blocks: []hcl.BlockHeaderSchema{
+ {
+ Type: "content",
+ LabelNames: nil,
+ },
+ },
+}
+
+var dynamicBlockBodySchemaNoLabels = &hcl.BodySchema{
+ Attributes: []hcl.AttributeSchema{
+ {
+ Name: "for_each",
+ Required: true,
+ },
+ {
+ Name: "iterator",
+ Required: false,
+ },
+ },
+ Blocks: []hcl.BlockHeaderSchema{
+ {
+ Type: "content",
+ LabelNames: nil,
+ },
+ },
+}
diff --git a/ext/dynblock/variables.go b/ext/dynblock/variables.go
new file mode 100644
index 0000000..245fb09
--- /dev/null
+++ b/ext/dynblock/variables.go
@@ -0,0 +1,72 @@
+package dynblock
+
+import (
+ "github.com/hashicorp/hcl2/hcl"
+)
+
+// ForEachVariables looks for "dynamic" blocks inside the given body
+// (which should be a body that would be passed to Expand, not the return
+// value of Expand) and returns any variables that are used within their
+// "for_each" and "labels" expressions, for use in dynamically constructing a
+// scope to pass as part of a hcl.EvalContext to Transformer.
+func ForEachVariables(original hcl.Body) []hcl.Traversal {
+ var traversals []hcl.Traversal
+ container, _, _ := original.PartialContent(variableDetectionContainerSchema)
+ if container == nil {
+ return traversals
+ }
+
+ for _, block := range container.Blocks {
+ inner, _, _ := block.Body.PartialContent(variableDetectionInnerSchema)
+ if inner == nil {
+ continue
+ }
+ iteratorName := block.Labels[0]
+ if attr, exists := inner.Attributes["iterator"]; exists {
+ iterTraversal, _ := hcl.AbsTraversalForExpr(attr.Expr)
+ if len(iterTraversal) > 0 {
+ iteratorName = iterTraversal.RootName()
+ }
+ }
+
+ if attr, exists := inner.Attributes["for_each"]; exists {
+ traversals = append(traversals, attr.Expr.Variables()...)
+ }
+ if attr, exists := inner.Attributes["labels"]; exists {
+ // Filter out our own iterator name, since the caller
+ // doesn't need to provide that.
+ for _, traversal := range attr.Expr.Variables() {
+ if traversal.RootName() != iteratorName {
+ traversals = append(traversals, traversal)
+ }
+ }
+ }
+ }
+
+ return traversals
+}
+
+// These are more-relaxed schemata than what's in schema.go, since we
+// want to maximize the amount of variables we can find even if there
+// are erroneous blocks.
+var variableDetectionContainerSchema = &hcl.BodySchema{
+ Blocks: []hcl.BlockHeaderSchema{
+ dynamicBlockHeaderSchema,
+ },
+}
+var variableDetectionInnerSchema = &hcl.BodySchema{
+ Attributes: []hcl.AttributeSchema{
+ {
+ Name: "for_each",
+ Required: false,
+ },
+ {
+ Name: "labels",
+ Required: false,
+ },
+ {
+ Name: "iterator",
+ Required: false,
+ },
+ },
+}