package dynblock import ( "fmt" "github.com/hashicorp/hcl/v2" "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 }