github.com/hashicorp/hcl/v2@v2.20.0/ext/dynblock/expand_spec.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package dynblock 5 6 import ( 7 "fmt" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/zclconf/go-cty/cty" 11 "github.com/zclconf/go-cty/cty/convert" 12 ) 13 14 type expandSpec struct { 15 blockType string 16 blockTypeRange hcl.Range 17 defRange hcl.Range 18 forEachVal cty.Value 19 iteratorName string 20 labelExprs []hcl.Expression 21 contentBody hcl.Body 22 inherited map[string]*iteration 23 } 24 25 func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandSpec, hcl.Diagnostics) { 26 var diags hcl.Diagnostics 27 28 var schema *hcl.BodySchema 29 if len(blockS.LabelNames) != 0 { 30 schema = dynamicBlockBodySchemaLabels 31 } else { 32 schema = dynamicBlockBodySchemaNoLabels 33 } 34 35 specContent, specDiags := rawSpec.Body.Content(schema) 36 diags = append(diags, specDiags...) 37 if specDiags.HasErrors() { 38 return nil, diags 39 } 40 41 //// iterator attribute 42 43 iteratorName := blockS.Type 44 if iteratorAttr := specContent.Attributes["iterator"]; iteratorAttr != nil { 45 itTraversal, itDiags := hcl.AbsTraversalForExpr(iteratorAttr.Expr) 46 diags = append(diags, itDiags...) 47 if itDiags.HasErrors() { 48 return nil, diags 49 } 50 51 if len(itTraversal) != 1 { 52 diags = append(diags, &hcl.Diagnostic{ 53 Severity: hcl.DiagError, 54 Summary: "Invalid dynamic iterator name", 55 Detail: "Dynamic iterator must be a single variable name.", 56 Subject: itTraversal.SourceRange().Ptr(), 57 }) 58 return nil, diags 59 } 60 61 iteratorName = itTraversal.RootName() 62 } 63 64 //// for_each attribute 65 66 eachAttr := specContent.Attributes["for_each"] 67 eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx) 68 diags = append(diags, eachDiags...) 69 if diags.HasErrors() { 70 return nil, diags 71 } 72 for _, check := range b.checkForEach { 73 moreDiags := check(eachVal, eachAttr.Expr, b.forEachCtx) 74 diags = append(diags, moreDiags...) 75 if moreDiags.HasErrors() { 76 return nil, diags 77 } 78 } 79 80 if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType { 81 // We skip this error for DynamicPseudoType because that means we either 82 // have a null (which is checked immediately below) or an unknown 83 // (which is handled in the expandBody Content methods). 84 diags = append(diags, &hcl.Diagnostic{ 85 Severity: hcl.DiagError, 86 Summary: "Invalid dynamic for_each value", 87 Detail: fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()), 88 Subject: eachAttr.Expr.Range().Ptr(), 89 Expression: eachAttr.Expr, 90 EvalContext: b.forEachCtx, 91 }) 92 return nil, diags 93 } 94 if eachVal.IsNull() { 95 diags = append(diags, &hcl.Diagnostic{ 96 Severity: hcl.DiagError, 97 Summary: "Invalid dynamic for_each value", 98 Detail: "Cannot use a null value in for_each.", 99 Subject: eachAttr.Expr.Range().Ptr(), 100 Expression: eachAttr.Expr, 101 EvalContext: b.forEachCtx, 102 }) 103 return nil, diags 104 } 105 106 //// labels attribute 107 108 var labelExprs []hcl.Expression 109 if labelsAttr := specContent.Attributes["labels"]; labelsAttr != nil { 110 var labelDiags hcl.Diagnostics 111 labelExprs, labelDiags = hcl.ExprList(labelsAttr.Expr) 112 diags = append(diags, labelDiags...) 113 if labelDiags.HasErrors() { 114 return nil, diags 115 } 116 117 if len(labelExprs) > len(blockS.LabelNames) { 118 diags = append(diags, &hcl.Diagnostic{ 119 Severity: hcl.DiagError, 120 Summary: "Extraneous dynamic block label", 121 Detail: fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)), 122 Subject: labelExprs[len(blockS.LabelNames)].Range().Ptr(), 123 }) 124 return nil, diags 125 } else if len(labelExprs) < len(blockS.LabelNames) { 126 diags = append(diags, &hcl.Diagnostic{ 127 Severity: hcl.DiagError, 128 Summary: "Insufficient dynamic block labels", 129 Detail: fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)), 130 Subject: labelsAttr.Expr.Range().Ptr(), 131 }) 132 return nil, diags 133 } 134 } 135 136 // Since our schema requests only blocks of type "content", we can assume 137 // that all entries in specContent.Blocks are content blocks. 138 if len(specContent.Blocks) == 0 { 139 diags = append(diags, &hcl.Diagnostic{ 140 Severity: hcl.DiagError, 141 Summary: "Missing dynamic content block", 142 Detail: "A dynamic block must have a nested block of type \"content\" to describe the body of each generated block.", 143 Subject: &specContent.MissingItemRange, 144 }) 145 return nil, diags 146 } 147 if len(specContent.Blocks) > 1 { 148 diags = append(diags, &hcl.Diagnostic{ 149 Severity: hcl.DiagError, 150 Summary: "Extraneous dynamic content block", 151 Detail: "Only one nested content block is allowed for each dynamic block.", 152 Subject: &specContent.Blocks[1].DefRange, 153 }) 154 return nil, diags 155 } 156 157 return &expandSpec{ 158 blockType: blockS.Type, 159 blockTypeRange: rawSpec.LabelRanges[0], 160 defRange: rawSpec.DefRange, 161 forEachVal: eachVal, 162 iteratorName: iteratorName, 163 labelExprs: labelExprs, 164 contentBody: specContent.Blocks[0].Body, 165 }, diags 166 } 167 168 func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) { 169 var diags hcl.Diagnostics 170 var labels []string 171 var labelRanges []hcl.Range 172 lCtx := i.EvalContext(ctx) 173 for _, labelExpr := range s.labelExprs { 174 labelVal, labelDiags := labelExpr.Value(lCtx) 175 diags = append(diags, labelDiags...) 176 if labelDiags.HasErrors() { 177 return nil, diags 178 } 179 180 var convErr error 181 labelVal, convErr = convert.Convert(labelVal, cty.String) 182 if convErr != nil { 183 diags = append(diags, &hcl.Diagnostic{ 184 Severity: hcl.DiagError, 185 Summary: "Invalid dynamic block label", 186 Detail: fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr), 187 Subject: labelExpr.Range().Ptr(), 188 Expression: labelExpr, 189 EvalContext: lCtx, 190 }) 191 return nil, diags 192 } 193 if labelVal.IsNull() { 194 diags = append(diags, &hcl.Diagnostic{ 195 Severity: hcl.DiagError, 196 Summary: "Invalid dynamic block label", 197 Detail: "Cannot use a null value as a dynamic block label.", 198 Subject: labelExpr.Range().Ptr(), 199 Expression: labelExpr, 200 EvalContext: lCtx, 201 }) 202 return nil, diags 203 } 204 if !labelVal.IsKnown() { 205 diags = append(diags, &hcl.Diagnostic{ 206 Severity: hcl.DiagError, 207 Summary: "Invalid dynamic block label", 208 Detail: "This value is not yet known. Dynamic block labels must be immediately-known values.", 209 Subject: labelExpr.Range().Ptr(), 210 Expression: labelExpr, 211 EvalContext: lCtx, 212 }) 213 return nil, diags 214 } 215 216 labels = append(labels, labelVal.AsString()) 217 labelRanges = append(labelRanges, labelExpr.Range()) 218 } 219 220 block := &hcl.Block{ 221 Type: s.blockType, 222 TypeRange: s.blockTypeRange, 223 Labels: labels, 224 LabelRanges: labelRanges, 225 DefRange: s.defRange, 226 Body: s.contentBody, 227 } 228 229 return block, diags 230 }