6631d7cd0a
Previously we were incorrectly passing down the original forEachCtx down to nested child blocks for recursive expansion. Instead, we must use the iteration-specific constructed EvalContext, which then allows any nested dynamic blocks to use the parent's iterator variable in their for_each or labels expressions, and thus unpack nested data structures into corresponding nested block structures: dynamic "parent" { for_each = [["a", "b"], []] content { dynamic "child" { for_each = parent.value content {} } } }
216 lines
6.4 KiB
Go
216 lines
6.4 KiB
Go
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() && eachVal.Type() != cty.DynamicPseudoType {
|
|
// We skip this error for DynamicPseudoType because that means we either
|
|
// have a null (which is checked immediately below) or an unknown
|
|
// (which is handled in the expandBody Content methods).
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid dynamic for_each value",
|
|
Detail: fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()),
|
|
Subject: eachAttr.Expr.Range().Ptr(),
|
|
Expression: eachAttr.Expr,
|
|
EvalContext: b.forEachCtx,
|
|
})
|
|
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(),
|
|
Expression: eachAttr.Expr,
|
|
EvalContext: b.forEachCtx,
|
|
})
|
|
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(),
|
|
Expression: labelExpr,
|
|
EvalContext: lCtx,
|
|
})
|
|
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(),
|
|
Expression: labelExpr,
|
|
EvalContext: lCtx,
|
|
})
|
|
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(),
|
|
Expression: labelExpr,
|
|
EvalContext: lCtx,
|
|
})
|
|
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
|
|
}
|