govendor fetch github.com/hashicorp/hcl2/ext/dynblock/...
This commit is contained in:
parent
63041ffa09
commit
f2809854a2
|
@ -0,0 +1,184 @@
|
|||
# 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.
|
||||
|
||||
## Detecting Variables
|
||||
|
||||
Some applications dynamically generate an `EvalContext` by analyzing which
|
||||
variables are referenced by an expression before evaluating it.
|
||||
|
||||
This unfortunately requires some extra effort when this analysis is required
|
||||
for the context passed to `Expand`: the HCL API requires a schema to be
|
||||
provided in order to do any analysis of the blocks in a body, but the low-level
|
||||
schema model provides a description of only one level of nested blocks at
|
||||
a time, and thus a new schema must be provided for each additional level of
|
||||
nesting.
|
||||
|
||||
To make this arduous process as convenient as possbile, this package provides
|
||||
a helper function `WalkForEachVariables`, which returns a `WalkVariablesNode`
|
||||
instance that can be used to find variables directly in a given body and also
|
||||
determine which nested blocks require recursive calls. Using this mechanism
|
||||
requires that the caller be able to look up a schema given a nested block type.
|
||||
For _simple_ formats where a specific block type name always has the same schema
|
||||
regardless of context, a walk can be implemented as follows:
|
||||
|
||||
```go
|
||||
func walkVariables(node dynblock.WalkVariablesNode, schema *hcl.BodySchema) []hcl.Traversal {
|
||||
vars, children := node.Visit(schema)
|
||||
|
||||
for _, child := range children {
|
||||
var childSchema *hcl.BodySchema
|
||||
switch child.BlockTypeName {
|
||||
case "a":
|
||||
childSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "b",
|
||||
LabelNames: []string{"key"},
|
||||
},
|
||||
},
|
||||
}
|
||||
case "b":
|
||||
childSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "val",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
// Should never happen, because the above cases should be exhaustive
|
||||
// for the application's configuration format.
|
||||
panic(fmt.Errorf("can't find schema for unknown block type %q", child.BlockTypeName))
|
||||
}
|
||||
|
||||
vars = append(vars, testWalkAndAccumVars(child.Node, childSchema)...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Detecting Variables with `hcldec` Specifications
|
||||
|
||||
For applications that use the higher-level `hcldec` package to decode nested
|
||||
configuration structures into `cty` values, the same specification can be used
|
||||
to automatically drive the recursive variable-detection walk described above.
|
||||
|
||||
The helper function `ForEachVariablesHCLDec` allows an entire recursive
|
||||
configuration structure to be analyzed in a single call given a `hcldec.Spec`
|
||||
that describes the nested block structure. This means a `hcldec`-based
|
||||
application can support dynamic blocks with only a little additional effort:
|
||||
|
||||
```go
|
||||
func decodeBody(body hcl.Body, spec hcldec.Spec) (cty.Value, hcl.Diagnostics) {
|
||||
// Determine which variables are needed to expand dynamic blocks
|
||||
neededForDynamic := dynblock.ForEachVariablesHCLDec(body, spec)
|
||||
|
||||
// Build a suitable EvalContext and expand dynamic blocks
|
||||
dynCtx := buildEvalContext(neededForDynamic)
|
||||
dynBody := dynblock.Expand(body, dynCtx)
|
||||
|
||||
// Determine which variables are needed to fully decode the expanded body
|
||||
// This will analyze expressions that came both from static blocks in the
|
||||
// original body and from blocks that were dynamically added by Expand.
|
||||
neededForDecode := hcldec.Variables(dynBody, spec)
|
||||
|
||||
// Build a suitable EvalContext and then fully decode the body as per the
|
||||
// hcldec specification.
|
||||
decCtx := buildEvalContext(neededForDecode)
|
||||
return hcldec.Decode(dynBody, spec, decCtx)
|
||||
}
|
||||
|
||||
func buildEvalContext(needed []hcl.Traversal) *hcl.EvalContext {
|
||||
// (to be implemented by your application)
|
||||
}
|
||||
```
|
||||
|
||||
# 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.
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
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)
|
||||
}
|
||||
|
||||
// UnwrapExpression returns the expression being wrapped by this instance.
|
||||
// This allows the original expression to be recovered by hcl.UnwrapExpression.
|
||||
func (e exprWrap) UnwrapExpression() hcl.Expression {
|
||||
return e.Expression
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package dynblock
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// WalkVariables begins the recursive process of walking the variables in the
|
||||
// given body that are needed by any "for_each" or "labels" attributes in
|
||||
// "dynamic" blocks. The result is a WalkVariablesNode, which can extract
|
||||
// root-level variable traversals and produce a list of child nodes that
|
||||
// also need to be processed by calling Visit.
|
||||
//
|
||||
// This function requires that the caller walk through the nested block
|
||||
// structure in the given body level-by-level so that an appropriate schema
|
||||
// can be provided at each level to inform further processing. This workflow
|
||||
// is thus easiest to use for calling applications that have some higher-level
|
||||
// schema representation available with which to drive this multi-step
|
||||
// process.
|
||||
func WalkForEachVariables(body hcl.Body) WalkVariablesNode {
|
||||
return WalkVariablesNode{
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
||||
type WalkVariablesNode struct {
|
||||
body hcl.Body
|
||||
it *iteration
|
||||
}
|
||||
|
||||
type WalkVariablesChild struct {
|
||||
BlockTypeName string
|
||||
Node WalkVariablesNode
|
||||
}
|
||||
|
||||
// Visit returns the variable traversals required for any "dynamic" blocks
|
||||
// directly in the body associated with this node, and also returns any child
|
||||
// nodes that must be visited in order to continue the walk.
|
||||
//
|
||||
// Each child node has its associated block type name given in its BlockTypeName
|
||||
// field, which the calling application should use to determine the appropriate
|
||||
// schema for the content of each child node and pass it to the child node's
|
||||
// own Visit method to continue the walk recursively.
|
||||
func (n WalkVariablesNode) Visit(schema *hcl.BodySchema) (vars []hcl.Traversal, children []WalkVariablesChild) {
|
||||
extSchema := n.extendSchema(schema)
|
||||
container, _, _ := n.body.PartialContent(extSchema)
|
||||
if container == nil {
|
||||
return vars, children
|
||||
}
|
||||
|
||||
children = make([]WalkVariablesChild, 0, len(container.Blocks))
|
||||
|
||||
for _, block := range container.Blocks {
|
||||
switch block.Type {
|
||||
|
||||
case "dynamic":
|
||||
blockTypeName := block.Labels[0]
|
||||
inner, _, _ := block.Body.PartialContent(variableDetectionInnerSchema)
|
||||
if inner == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
iteratorName := blockTypeName
|
||||
if attr, exists := inner.Attributes["iterator"]; exists {
|
||||
iterTraversal, _ := hcl.AbsTraversalForExpr(attr.Expr)
|
||||
if len(iterTraversal) == 0 {
|
||||
// Ignore this invalid dynamic block, since it'll produce
|
||||
// an error if someone tries to extract content from it
|
||||
// later anyway.
|
||||
continue
|
||||
}
|
||||
iteratorName = iterTraversal.RootName()
|
||||
}
|
||||
blockIt := n.it.MakeChild(iteratorName, cty.DynamicVal, cty.DynamicVal)
|
||||
|
||||
if attr, exists := inner.Attributes["for_each"]; exists {
|
||||
// Filter out iterator names inherited from parent blocks
|
||||
for _, traversal := range attr.Expr.Variables() {
|
||||
if _, inherited := blockIt.Inherited[traversal.RootName()]; !inherited {
|
||||
vars = append(vars, traversal)
|
||||
}
|
||||
}
|
||||
}
|
||||
if attr, exists := inner.Attributes["labels"]; exists {
|
||||
// Filter out both our own iterator name _and_ those inherited
|
||||
// from parent blocks, since we provide _both_ of these to the
|
||||
// label expressions.
|
||||
for _, traversal := range attr.Expr.Variables() {
|
||||
ours := traversal.RootName() == iteratorName
|
||||
_, inherited := blockIt.Inherited[traversal.RootName()]
|
||||
|
||||
if !(ours || inherited) {
|
||||
vars = append(vars, traversal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, contentBlock := range inner.Blocks {
|
||||
// We only request "content" blocks in our schema, so we know
|
||||
// any blocks we find here will be content blocks. We require
|
||||
// exactly one content block for actual expansion, but we'll
|
||||
// be more liberal here so that callers can still collect
|
||||
// variables from erroneous "dynamic" blocks.
|
||||
children = append(children, WalkVariablesChild{
|
||||
BlockTypeName: blockTypeName,
|
||||
Node: WalkVariablesNode{
|
||||
body: contentBlock.Body,
|
||||
it: blockIt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
children = append(children, WalkVariablesChild{
|
||||
BlockTypeName: block.Type,
|
||||
Node: WalkVariablesNode{
|
||||
body: block.Body,
|
||||
it: n.it,
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return vars, children
|
||||
}
|
||||
|
||||
func (n WalkVariablesNode) 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)+1),
|
||||
}
|
||||
copy(extSchema.Blocks, schema.Blocks)
|
||||
extSchema.Blocks = append(extSchema.Blocks, dynamicBlockHeaderSchema)
|
||||
|
||||
return extSchema
|
||||
}
|
||||
|
||||
// This is a more relaxed schema 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 variableDetectionInnerSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "for_each",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "labels",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Name: "iterator",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "content",
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package dynblock
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcldec"
|
||||
)
|
||||
|
||||
// ForEachVariablesHCLDec is a wrapper around WalkForEachVariables that
|
||||
// uses the given hcldec specification to automatically drive the recursive
|
||||
// walk through nested blocks in the given body.
|
||||
//
|
||||
// This provides more convenient access to all of the "for_each" and "labels"
|
||||
// dependencies in a body for applications that are already using hcldec
|
||||
// as a more convenient way to recursively decode body contents.
|
||||
func ForEachVariablesHCLDec(body hcl.Body, spec hcldec.Spec) []hcl.Traversal {
|
||||
rootNode := WalkForEachVariables(body)
|
||||
return walkVariablesWithHCLDec(rootNode, spec)
|
||||
}
|
||||
|
||||
func walkVariablesWithHCLDec(node WalkVariablesNode, spec hcldec.Spec) []hcl.Traversal {
|
||||
vars, children := node.Visit(hcldec.ImpliedSchema(spec))
|
||||
|
||||
if len(children) > 0 {
|
||||
childSpecs := hcldec.ChildBlockTypes(spec)
|
||||
for _, child := range children {
|
||||
if childSpec, exists := childSpecs[child.BlockTypeName]; exists {
|
||||
vars = append(vars, walkVariablesWithHCLDec(child.Node, childSpec)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
|
@ -1888,6 +1888,12 @@
|
|||
"revision": "a4b07c25de5ff55ad3b8936cea69a79a3d95a855",
|
||||
"revisionTime": "2017-05-04T19:02:34Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "dJPromzLdd492RQjE/09klKRXGs=",
|
||||
"path": "github.com/hashicorp/hcl2/ext/dynblock",
|
||||
"revision": "5f8ed954abd873b2c09616ba0aa607892bbca7e9",
|
||||
"revisionTime": "2018-03-08T16:30:58Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Tpj2tK/XrhxbIKB5xEJlfTI46M0=",
|
||||
"path": "github.com/hashicorp/hcl2/ext/typeexpr",
|
||||
|
|
Loading…
Reference in New Issue