github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/tfhcl/expand_spec.go (about) 1 package tfhcl 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/terraform-linters/tflint/terraform/tfdiags" 8 "github.com/zclconf/go-cty/cty" 9 "github.com/zclconf/go-cty/cty/convert" 10 "github.com/zclconf/go-cty/cty/gocty" 11 ) 12 13 type expandDynamicSpec struct { 14 blockType string 15 blockTypeRange hcl.Range 16 defRange hcl.Range 17 forEachVal cty.Value 18 iteratorName string 19 labelExprs []hcl.Expression 20 contentBody hcl.Body 21 } 22 23 func (b *expandBody) decodeDynamicSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandDynamicSpec, hcl.Diagnostics) { 24 var diags hcl.Diagnostics 25 26 var schema *hcl.BodySchema 27 if len(blockS.LabelNames) != 0 { 28 schema = dynamicBlockBodySchemaLabels 29 } else { 30 schema = dynamicBlockBodySchemaNoLabels 31 } 32 33 specContent, specDiags := rawSpec.Body.Content(schema) 34 diags = append(diags, specDiags...) 35 if specDiags.HasErrors() { 36 return nil, diags 37 } 38 39 //// for_each attribute 40 41 eachAttr := specContent.Attributes["for_each"] 42 eachVal, eachDiags := eachAttr.Expr.Value(b.ctx) 43 diags = append(diags, eachDiags...) 44 45 if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType { 46 // We skip this error for DynamicPseudoType because that means we either 47 // have a null (which is checked immediately below) or an unknown 48 // (which is handled in the expandBody Content methods). 49 diags = append(diags, &hcl.Diagnostic{ 50 Severity: hcl.DiagError, 51 Summary: "Invalid dynamic for_each value", 52 Detail: fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()), 53 Subject: eachAttr.Expr.Range().Ptr(), 54 Expression: eachAttr.Expr, 55 EvalContext: b.ctx, 56 }) 57 return nil, diags 58 } 59 if eachVal.IsNull() { 60 diags = append(diags, &hcl.Diagnostic{ 61 Severity: hcl.DiagError, 62 Summary: "Invalid dynamic for_each value", 63 Detail: "Cannot use a null value in for_each.", 64 Subject: eachAttr.Expr.Range().Ptr(), 65 Expression: eachAttr.Expr, 66 EvalContext: b.ctx, 67 }) 68 return nil, diags 69 } 70 71 //// iterator attribute 72 73 iteratorName := blockS.Type 74 if iteratorAttr := specContent.Attributes["iterator"]; iteratorAttr != nil { 75 itTraversal, itDiags := hcl.AbsTraversalForExpr(iteratorAttr.Expr) 76 diags = append(diags, itDiags...) 77 if itDiags.HasErrors() { 78 return nil, diags 79 } 80 81 if len(itTraversal) != 1 { 82 diags = append(diags, &hcl.Diagnostic{ 83 Severity: hcl.DiagError, 84 Summary: "Invalid dynamic iterator name", 85 Detail: "Dynamic iterator must be a single variable name.", 86 Subject: itTraversal.SourceRange().Ptr(), 87 }) 88 return nil, diags 89 } 90 91 iteratorName = itTraversal.RootName() 92 } 93 94 var labelExprs []hcl.Expression 95 if labelsAttr := specContent.Attributes["labels"]; labelsAttr != nil { 96 var labelDiags hcl.Diagnostics 97 labelExprs, labelDiags = hcl.ExprList(labelsAttr.Expr) 98 diags = append(diags, labelDiags...) 99 if labelDiags.HasErrors() { 100 return nil, diags 101 } 102 103 if len(labelExprs) > len(blockS.LabelNames) { 104 diags = append(diags, &hcl.Diagnostic{ 105 Severity: hcl.DiagError, 106 Summary: "Extraneous dynamic block label", 107 Detail: fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)), 108 Subject: labelExprs[len(blockS.LabelNames)].Range().Ptr(), 109 }) 110 return nil, diags 111 } else if len(labelExprs) < len(blockS.LabelNames) { 112 diags = append(diags, &hcl.Diagnostic{ 113 Severity: hcl.DiagError, 114 Summary: "Insufficient dynamic block labels", 115 Detail: fmt.Sprintf("Blocks of type %q require %d label(s).", blockS.Type, len(blockS.LabelNames)), 116 Subject: labelsAttr.Expr.Range().Ptr(), 117 }) 118 return nil, diags 119 } 120 } 121 122 // Since our schema requests only blocks of type "content", we can assume 123 // that all entries in specContent.Blocks are content blocks. 124 if len(specContent.Blocks) == 0 { 125 diags = append(diags, &hcl.Diagnostic{ 126 Severity: hcl.DiagError, 127 Summary: "Missing dynamic content block", 128 Detail: "A dynamic block must have a nested block of type \"content\" to describe the body of each generated block.", 129 Subject: &specContent.MissingItemRange, 130 }) 131 return nil, diags 132 } 133 if len(specContent.Blocks) > 1 { 134 diags = append(diags, &hcl.Diagnostic{ 135 Severity: hcl.DiagError, 136 Summary: "Extraneous dynamic content block", 137 Detail: "Only one nested content block is allowed for each dynamic block.", 138 Subject: &specContent.Blocks[1].DefRange, 139 }) 140 return nil, diags 141 } 142 143 return &expandDynamicSpec{ 144 blockType: blockS.Type, 145 blockTypeRange: rawSpec.LabelRanges[0], 146 defRange: rawSpec.DefRange, 147 forEachVal: eachVal, 148 iteratorName: iteratorName, 149 labelExprs: labelExprs, 150 contentBody: specContent.Blocks[0].Body, 151 }, diags 152 } 153 154 func (s *expandDynamicSpec) newBlock(i *dynamicIteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) { 155 var diags hcl.Diagnostics 156 var labels []string 157 var labelRanges []hcl.Range 158 lCtx := i.EvalContext(ctx) 159 for _, labelExpr := range s.labelExprs { 160 labelVal, labelDiags := labelExpr.Value(lCtx) 161 diags = append(diags, labelDiags...) 162 if labelDiags.HasErrors() { 163 return nil, diags 164 } 165 166 var convErr error 167 labelVal, convErr = convert.Convert(labelVal, cty.String) 168 if convErr != nil { 169 diags = append(diags, &hcl.Diagnostic{ 170 Severity: hcl.DiagError, 171 Summary: "Invalid dynamic block label", 172 Detail: fmt.Sprintf("Cannot use this value as a dynamic block label: %s.", convErr), 173 Subject: labelExpr.Range().Ptr(), 174 Expression: labelExpr, 175 EvalContext: lCtx, 176 }) 177 return nil, diags 178 } 179 if labelVal.IsNull() { 180 diags = append(diags, &hcl.Diagnostic{ 181 Severity: hcl.DiagError, 182 Summary: "Invalid dynamic block label", 183 Detail: "Cannot use a null value as a dynamic block label.", 184 Subject: labelExpr.Range().Ptr(), 185 Expression: labelExpr, 186 EvalContext: lCtx, 187 }) 188 return nil, diags 189 } 190 if !labelVal.IsKnown() { 191 return nil, diags 192 } 193 if labelVal.IsMarked() { 194 diags = append(diags, &hcl.Diagnostic{ 195 Severity: hcl.DiagError, 196 Summary: "Invalid dynamic block label", 197 Detail: "Cannot use a marked value as a dynamic block label.", 198 Subject: labelExpr.Range().Ptr(), 199 Expression: labelExpr, 200 EvalContext: lCtx, 201 }) 202 return nil, diags 203 } 204 205 labels = append(labels, labelVal.AsString()) 206 labelRanges = append(labelRanges, labelExpr.Range()) 207 } 208 209 block := &hcl.Block{ 210 Type: s.blockType, 211 TypeRange: s.blockTypeRange, 212 Labels: labels, 213 LabelRanges: labelRanges, 214 DefRange: s.defRange, 215 Body: s.contentBody, 216 } 217 218 return block, diags 219 } 220 221 type expandMetaArgSpec struct { 222 rawBlock *hcl.Block 223 countSet bool 224 countVal cty.Value 225 countNum int 226 forEachSet bool 227 forEachVal cty.Value 228 } 229 230 func (b *expandBody) decodeMetaArgSpec(rawSpec *hcl.Block) (*expandMetaArgSpec, hcl.Diagnostics) { 231 spec := &expandMetaArgSpec{rawBlock: rawSpec} 232 var diags hcl.Diagnostics 233 234 specContent, _, specDiags := rawSpec.Body.PartialContent(expandableBlockBodySchema) 235 diags = append(diags, specDiags...) 236 if specDiags.HasErrors() { 237 return spec, diags 238 } 239 240 //// count attribute 241 242 if countAttr, exists := specContent.Attributes["count"]; exists { 243 spec.countSet = true 244 245 countVal, countDiags := countAttr.Expr.Value(b.ctx) 246 diags = append(diags, countDiags...) 247 countVal, _ = countVal.Unmark() 248 249 spec.countVal = countVal 250 251 // We skip validation for count attribute if the value is unknwon 252 if countVal.IsKnown() { 253 if countVal.IsNull() { 254 diags = append(diags, &hcl.Diagnostic{ 255 Severity: hcl.DiagError, 256 Summary: "Invalid count argument", 257 Detail: `The given "count" argument value is null. An integer is required.`, 258 Subject: countAttr.Expr.Range().Ptr(), 259 Expression: countAttr.Expr, 260 EvalContext: b.ctx, 261 }) 262 return spec, diags 263 } 264 265 var convErr error 266 countVal, convErr = convert.Convert(countVal, cty.Number) 267 if convErr != nil { 268 diags = diags.Append(&hcl.Diagnostic{ 269 Severity: hcl.DiagError, 270 Summary: "Incorrect value type", 271 Detail: fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)), 272 Subject: countAttr.Expr.Range().Ptr(), 273 Expression: countAttr.Expr, 274 EvalContext: b.ctx, 275 }) 276 return spec, diags 277 } 278 279 err := gocty.FromCtyValue(countVal, &spec.countNum) 280 if err != nil { 281 diags = diags.Append(&hcl.Diagnostic{ 282 Severity: hcl.DiagError, 283 Summary: "Invalid count argument", 284 Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), 285 Subject: countAttr.Expr.Range().Ptr(), 286 Expression: countAttr.Expr, 287 EvalContext: b.ctx, 288 }) 289 return spec, diags 290 } 291 if spec.countNum < 0 { 292 diags = diags.Append(&hcl.Diagnostic{ 293 Severity: hcl.DiagError, 294 Summary: "Invalid count argument", 295 Detail: `The given "count" argument value is unsuitable: negative numbers are not supported.`, 296 Subject: countAttr.Expr.Range().Ptr(), 297 Expression: countAttr.Expr, 298 EvalContext: b.ctx, 299 }) 300 return spec, diags 301 } 302 } 303 } 304 305 //// for_each attribute 306 307 if eachAttr, exists := specContent.Attributes["for_each"]; exists { 308 spec.forEachSet = true 309 310 eachVal, eachDiags := eachAttr.Expr.Value(b.ctx) 311 diags = append(diags, eachDiags...) 312 313 spec.forEachVal = eachVal 314 315 if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType { 316 // We skip this error for DynamicPseudoType because that means we either 317 // have a null (which is checked immediately below) or an unknown 318 // (which is handled in the expandBody Content methods). 319 diags = diags.Append(&hcl.Diagnostic{ 320 Severity: hcl.DiagError, 321 Summary: "The `for_each` value is not iterable", 322 Detail: fmt.Sprintf("`%s` is not iterable", eachVal.GoString()), 323 Subject: eachAttr.Expr.Range().Ptr(), 324 Expression: eachAttr.Expr, 325 EvalContext: b.ctx, 326 }) 327 return spec, diags 328 } 329 if eachVal.IsNull() { 330 diags = diags.Append(&hcl.Diagnostic{ 331 Severity: hcl.DiagError, 332 Summary: "Invalid for_each argument", 333 Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`, 334 Subject: eachAttr.Expr.Range().Ptr(), 335 Expression: eachAttr.Expr, 336 EvalContext: b.ctx, 337 }) 338 return spec, diags 339 } 340 } 341 342 return spec, diags 343 }