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.
This commit is contained in:
parent
f87600a7d9
commit
da95646a33
99
ext/dynblock/README.md
Normal file
99
ext/dynblock/README.md
Normal file
@ -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.
|
251
ext/dynblock/expand_body.go
Normal file
251
ext/dynblock/expand_body.go
Normal file
@ -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()
|
||||
}
|
278
ext/dynblock/expand_body_test.go
Normal file
278
ext/dynblock/expand_body_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
202
ext/dynblock/expand_spec.go
Normal file
202
ext/dynblock/expand_spec.go
Normal file
@ -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
|
||||
}
|
60
ext/dynblock/expr_wrap.go
Normal file
60
ext/dynblock/expr_wrap.go
Normal file
@ -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
|
||||
}
|
64
ext/dynblock/iteration.go
Normal file
64
ext/dynblock/iteration.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
44
ext/dynblock/public.go
Normal file
44
ext/dynblock/public.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
50
ext/dynblock/schema.go
Normal file
50
ext/dynblock/schema.go
Normal file
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
72
ext/dynblock/variables.go
Normal file
72
ext/dynblock/variables.go
Normal file
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue
Block a user