github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/upgrade_body.go (about) 1 package configupgrade 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sort" 9 "strconv" 10 "strings" 11 12 hcl1ast "github.com/hashicorp/hcl/hcl/ast" 13 hcl1token "github.com/hashicorp/hcl/hcl/token" 14 hcl2 "github.com/hashicorp/hcl/v2" 15 hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax" 16 "github.com/hashicorp/terraform/configs/configschema" 17 "github.com/hashicorp/terraform/lang/blocktoattr" 18 "github.com/hashicorp/terraform/registry/regsrc" 19 "github.com/hashicorp/terraform/terraform" 20 "github.com/hashicorp/terraform/tfdiags" 21 "github.com/zclconf/go-cty/cty" 22 ) 23 24 // bodyContentRules is a mapping from item names (argument names and block type 25 // names) to a "rule" function defining what to do with an item of that type. 26 type bodyContentRules map[string]bodyItemRule 27 28 // bodyItemRule is just a function to write an upgraded representation of a 29 // particular given item to the given buffer. This is generic to handle various 30 // different mapping rules, though most values will be those constructed by 31 // other helper functions below. 32 type bodyItemRule func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics 33 34 func normalAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule { 35 exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { 36 return upgradeExpr(val, filename, true, an) 37 } 38 return attributeRule(filename, wantTy, an, exprRule) 39 } 40 41 func noInterpAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule { 42 exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { 43 return upgradeExpr(val, filename, false, an) 44 } 45 return attributeRule(filename, wantTy, an, exprRule) 46 } 47 48 func maybeBareKeywordAttributeRule(filename string, an *analysis, specials map[string]string) bodyItemRule { 49 exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { 50 // If the expression is a literal that would be valid as a naked keyword 51 // then we'll turn it into one. 52 if lit, isLit := val.(*hcl1ast.LiteralType); isLit { 53 if lit.Token.Type == hcl1token.STRING { 54 kw := lit.Token.Value().(string) 55 if hcl2syntax.ValidIdentifier(kw) { 56 57 // If we have a special mapping rule for this keyword, 58 // we'll let that override what the user gave. 59 if override := specials[kw]; override != "" { 60 kw = override 61 } 62 63 return []byte(kw), nil 64 } 65 } 66 } 67 68 return upgradeExpr(val, filename, false, an) 69 } 70 return attributeRule(filename, cty.String, an, exprRule) 71 } 72 73 func maybeBareTraversalAttributeRule(filename string, an *analysis) bodyItemRule { 74 exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { 75 return upgradeTraversalExpr(val, filename, an) 76 } 77 return attributeRule(filename, cty.String, an, exprRule) 78 } 79 80 func dependsOnAttributeRule(filename string, an *analysis) bodyItemRule { 81 return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 82 var diags tfdiags.Diagnostics 83 val, ok := item.Val.(*hcl1ast.ListType) 84 if !ok { 85 diags = diags.Append(&hcl2.Diagnostic{ 86 Severity: hcl2.DiagError, 87 Summary: "Invalid depends_on argument", 88 Detail: `The "depends_on" argument must be a list of strings containing references to resources and modules.`, 89 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 90 }) 91 return diags 92 } 93 94 var exprBuf bytes.Buffer 95 multiline := len(val.List) > 1 96 exprBuf.WriteByte('[') 97 if multiline { 98 exprBuf.WriteByte('\n') 99 } 100 for _, node := range val.List { 101 lit, ok := node.(*hcl1ast.LiteralType) 102 if (!ok) || lit.Token.Type != hcl1token.STRING { 103 diags = diags.Append(&hcl2.Diagnostic{ 104 Severity: hcl2.DiagError, 105 Summary: "Invalid depends_on argument", 106 Detail: `The "depends_on" argument must be a list of strings containing references to resources and modules.`, 107 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 108 }) 109 continue 110 } 111 refStr := lit.Token.Value().(string) 112 if refStr == "" { 113 continue 114 } 115 refParts := strings.Split(refStr, ".") 116 var maxNames int 117 switch refParts[0] { 118 case "data", "module": 119 maxNames = 3 120 default: // resource references 121 maxNames = 2 122 } 123 124 exprBuf.WriteString(refParts[0]) 125 for i, part := range refParts[1:] { 126 if part == "*" { 127 // We used to allow test_instance.foo.* as a reference 128 // but now that's expressed instead as test_instance.foo, 129 // referring to the tuple of instances. This also 130 // always marks the end of the reference part of the 131 // traversal, so anything after this would be resource 132 // attributes that don't belong on depends_on. 133 break 134 } 135 if i, err := strconv.Atoi(part); err == nil { 136 fmt.Fprintf(&exprBuf, "[%d]", i) 137 // An index always marks the end of the reference part. 138 break 139 } 140 if (i + 1) >= maxNames { 141 // We've reached the end of the reference part, so anything 142 // after this would be invalid in 0.12. 143 break 144 } 145 exprBuf.WriteByte('.') 146 exprBuf.WriteString(part) 147 } 148 149 if multiline { 150 exprBuf.WriteString(",\n") 151 } 152 } 153 exprBuf.WriteByte(']') 154 155 printAttribute(buf, item.Keys[0].Token.Value().(string), exprBuf.Bytes(), item.LineComment) 156 157 return diags 158 } 159 } 160 161 func attributeRule(filename string, wantTy cty.Type, an *analysis, exprRule func(val interface{}) ([]byte, tfdiags.Diagnostics)) bodyItemRule { 162 return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 163 var diags tfdiags.Diagnostics 164 165 name := item.Keys[0].Token.Value().(string) 166 167 // We'll tolerate a block with no labels here as a degenerate 168 // way to assign a map, but we can't migrate a block that has 169 // labels. In practice this should never happen because 170 // nested blocks in resource blocks did not accept labels 171 // prior to v0.12. 172 if len(item.Keys) != 1 { 173 diags = diags.Append(&hcl2.Diagnostic{ 174 Severity: hcl2.DiagError, 175 Summary: "Block where attribute was expected", 176 Detail: fmt.Sprintf("Within %s the name %q is an attribute name, not a block type.", blockAddr, name), 177 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 178 }) 179 return diags 180 } 181 182 val := item.Val 183 184 if typeIsSettableFromTupleCons(wantTy) && !typeIsSettableFromTupleCons(wantTy.ElementType()) { 185 // In Terraform circa 0.10 it was required to wrap any expression 186 // that produces a list in HCL list brackets to allow type analysis 187 // to complete successfully, even though logically that ought to 188 // have produced a list of lists. 189 // 190 // In Terraform 0.12 this construct _does_ produce a list of lists, so 191 // we need to update expressions that look like older usage. We can't 192 // do this exactly with static analysis, but we can make a best-effort 193 // and produce a warning if type inference is impossible for a 194 // particular expression. This should give good results for common 195 // simple examples, like splat expressions. 196 // 197 // There are four possible cases here: 198 // - The value isn't an HCL1 list expression at all, or is one that 199 // contains more than one item, in which case this special case 200 // does not apply. 201 // - The inner expression after upgrading can be proven to return 202 // a sequence type, in which case we must definitely remove 203 // the wrapping brackets. 204 // - The inner expression after upgrading can be proven to return 205 // a non-sequence type, in which case we fall through and treat 206 // the whole item like a normal expression. 207 // - Static type analysis is impossible (it returns cty.DynamicPseudoType), 208 // in which case we will make no changes but emit a warning and 209 // a TODO comment for the user to decide whether a change needs 210 // to be made in practice. 211 if list, ok := val.(*hcl1ast.ListType); ok { 212 if len(list.List) == 1 { 213 maybeAlsoList := list.List[0] 214 if exprSrc, diags := upgradeExpr(maybeAlsoList, filename, true, an); !diags.HasErrors() { 215 // Ideally we would set "self" here but we don't have 216 // enough context to set it and in practice not setting 217 // it only affects expressions inside provisioner and 218 // connection blocks, and the list-wrapping thing isn't 219 // common there. 220 gotTy := an.InferExpressionType(exprSrc, nil) 221 if typeIsSettableFromTupleCons(gotTy) { 222 // Since this expression was already inside HCL list brackets, 223 // the ultimate result would be a list of lists and so we 224 // need to unwrap it by taking just the portion within 225 // the brackets here. 226 val = maybeAlsoList 227 } 228 if gotTy == cty.DynamicPseudoType { 229 // User must decide. 230 diags = diags.Append(&hcl2.Diagnostic{ 231 Severity: hcl2.DiagError, 232 Summary: "Possible legacy dynamic list usage", 233 Detail: "This list may be using the legacy redundant-list expression style from Terraform v0.10 and earlier. If the expression within these brackets returns a list itself, remove these brackets.", 234 Subject: hcl1PosRange(filename, list.Lbrack).Ptr(), 235 }) 236 buf.WriteString( 237 "# TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to\n" + 238 "# force an interpolation expression to be interpreted as a list by wrapping it\n" + 239 "# in an extra set of list brackets. That form was supported for compatibility in\n" + 240 "# v0.11, but is no longer supported in Terraform v0.12.\n" + 241 "#\n" + 242 "# If the expression in the following list itself returns a list, remove the\n" + 243 "# brackets to avoid interpretation as a list of lists. If the expression\n" + 244 "# returns a single list item then leave it as-is and remove this TODO comment.\n", 245 ) 246 } 247 } 248 } 249 } 250 } 251 252 valSrc, valDiags := exprRule(val) 253 diags = diags.Append(valDiags) 254 printAttribute(buf, item.Keys[0].Token.Value().(string), valSrc, item.LineComment) 255 256 return diags 257 } 258 } 259 260 func nestedBlockRule(filename string, nestedRules bodyContentRules, an *analysis, adhocComments *commentQueue) bodyItemRule { 261 return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 262 // This simpler nestedBlockRule is for contexts where the special 263 // "dynamic" block type is not accepted and so only HCL1 object 264 // constructs can be accepted. Attempts to assign arbitrary HIL 265 // expressions will be rejected as errors. 266 267 var diags tfdiags.Diagnostics 268 declRange := hcl1PosRange(filename, item.Keys[0].Pos()) 269 blockType := item.Keys[0].Token.Value().(string) 270 labels := make([]string, len(item.Keys)-1) 271 for i, key := range item.Keys[1:] { 272 labels[i] = key.Token.Value().(string) 273 } 274 275 var blockItems []*hcl1ast.ObjectType 276 277 switch val := item.Val.(type) { 278 279 case *hcl1ast.ObjectType: 280 blockItems = []*hcl1ast.ObjectType{val} 281 282 case *hcl1ast.ListType: 283 for _, node := range val.List { 284 switch listItem := node.(type) { 285 case *hcl1ast.ObjectType: 286 blockItems = append(blockItems, listItem) 287 default: 288 diags = diags.Append(&hcl2.Diagnostic{ 289 Severity: hcl2.DiagError, 290 Summary: "Invalid value for nested block", 291 Detail: fmt.Sprintf("In %s the name %q is a nested block type, so any value assigned to it must be an object.", blockAddr, blockType), 292 Subject: hcl1PosRange(filename, node.Pos()).Ptr(), 293 }) 294 } 295 } 296 297 default: 298 diags = diags.Append(&hcl2.Diagnostic{ 299 Severity: hcl2.DiagError, 300 Summary: "Invalid value for nested block", 301 Detail: fmt.Sprintf("In %s the name %q is a nested block type, so any value assigned to it must be an object.", blockAddr, blockType), 302 Subject: &declRange, 303 }) 304 return diags 305 } 306 307 for _, blockItem := range blockItems { 308 printBlockOpen(buf, blockType, labels, item.LineComment) 309 bodyDiags := upgradeBlockBody( 310 filename, fmt.Sprintf("%s.%s", blockAddr, blockType), buf, 311 blockItem.List.Items, blockItem.Rbrace, nestedRules, adhocComments, 312 ) 313 diags = diags.Append(bodyDiags) 314 buf.WriteString("}\n") 315 } 316 317 return diags 318 } 319 } 320 321 func nestedBlockRuleWithDynamic(filename string, nestedRules bodyContentRules, nestedSchema *configschema.NestedBlock, emptyAsAttr bool, an *analysis, adhocComments *commentQueue) bodyItemRule { 322 return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 323 // In Terraform v0.11 it was possible in some cases to trick Terraform 324 // and providers into accepting HCL's attribute syntax and some HIL 325 // expressions in places where blocks or sequences of blocks were 326 // expected, since the information about the heritage of the values 327 // was lost during decoding and interpolation. 328 // 329 // In order to avoid all of the weird rough edges that resulted from 330 // those misinterpretations, Terraform v0.12 is stricter and requires 331 // the use of block syntax for blocks in all cases. However, because 332 // various abuses of attribute syntax _did_ work (with some caveats) 333 // in v0.11 we will upgrade them as best we can to use proper block 334 // syntax. 335 // 336 // There are a few different permutations supported by this code: 337 // 338 // - Assigning a single HCL1 "object" using attribute syntax. This is 339 // straightforward to migrate just by dropping the equals sign. 340 // 341 // - Assigning a HCL1 list of objects using attribute syntax. Each 342 // object in that list can be translated to a separate block. 343 // 344 // - Assigning a HCL1 list containing HIL expressions that evaluate 345 // to maps. This is a hard case because we can't know the internal 346 // structure of those maps during static analysis, and so we must 347 // generate a worst-case dynamic block structure for it. 348 // 349 // - Assigning a single HIL expression that evaluates to a list of 350 // maps. This is just like the previous case except additionally 351 // we cannot even predict the number of generated blocks, so we must 352 // generate a single "dynamic" block to iterate over the list at 353 // runtime. 354 355 var diags tfdiags.Diagnostics 356 blockType := item.Keys[0].Token.Value().(string) 357 labels := make([]string, len(item.Keys)-1) 358 for i, key := range item.Keys[1:] { 359 labels[i] = key.Token.Value().(string) 360 } 361 362 var blockItems []hcl1ast.Node 363 364 switch val := item.Val.(type) { 365 366 case *hcl1ast.ObjectType: 367 blockItems = append(blockItems, val) 368 369 case *hcl1ast.ListType: 370 for _, node := range val.List { 371 switch listItem := node.(type) { 372 case *hcl1ast.ObjectType: 373 blockItems = append(blockItems, listItem) 374 default: 375 // We're going to cheat a bit here and construct a synthetic 376 // HCL1 list just because that makes our logic 377 // simpler below where we can just treat all non-objects 378 // in the same way when producing "dynamic" blocks. 379 synthList := &hcl1ast.ListType{ 380 List: []hcl1ast.Node{listItem}, 381 Lbrack: listItem.Pos(), 382 Rbrack: hcl1NodeEndPos(listItem), 383 } 384 blockItems = append(blockItems, synthList) 385 } 386 } 387 388 default: 389 blockItems = append(blockItems, item.Val) 390 } 391 392 if len(blockItems) == 0 && emptyAsAttr { 393 // Terraform v0.12's config decoder allows using block syntax for 394 // certain attribute types, which we prefer as idiomatic usage 395 // causing us to end up in this function in such cases, but as 396 // a special case users can still use the attribute syntax to 397 // explicitly write an empty list. For more information, see 398 // the lang/blocktoattr package. 399 printAttribute(buf, item.Keys[0].Token.Value().(string), []byte{'[', ']'}, item.LineComment) 400 return diags 401 } 402 403 for _, blockItem := range blockItems { 404 switch ti := blockItem.(type) { 405 case *hcl1ast.ObjectType: 406 // If we have an object then we'll pass through its content 407 // as a block directly. This is the most straightforward mapping 408 // from the source input, since we know exactly which keys 409 // are present. 410 printBlockOpen(buf, blockType, labels, item.LineComment) 411 bodyDiags := upgradeBlockBody( 412 filename, fmt.Sprintf("%s.%s", blockAddr, blockType), buf, 413 ti.List.Items, ti.Rbrace, nestedRules, adhocComments, 414 ) 415 diags = diags.Append(bodyDiags) 416 buf.WriteString("}\n") 417 default: 418 // For any other sort of value we can't predict what shape it 419 // will have at runtime, so we must generate a very conservative 420 // "dynamic" block that tries to assign everything from the 421 // schema. The result of this is likely to be pretty ugly. 422 printBlockOpen(buf, "dynamic", []string{blockType}, item.LineComment) 423 eachSrc, eachDiags := upgradeExpr(blockItem, filename, true, an) 424 diags = diags.Append(eachDiags) 425 printAttribute(buf, "for_each", eachSrc, nil) 426 if nestedSchema.Nesting == configschema.NestingMap { 427 // This is a pretty odd situation since map-based blocks 428 // didn't exist prior to Terraform v0.12, but we'll support 429 // this anyway in case we decide to add support in a later 430 // SDK release that is still somehow compatible with 431 // Terraform v0.11. 432 printAttribute(buf, "labels", []byte(fmt.Sprintf(`[%s.key]`, blockType)), nil) 433 } 434 printBlockOpen(buf, "content", nil, nil) 435 buf.WriteString("# TF-UPGRADE-TODO: The automatic upgrade tool can't predict\n") 436 buf.WriteString("# which keys might be set in maps assigned here, so it has\n") 437 buf.WriteString("# produced a comprehensive set here. Consider simplifying\n") 438 buf.WriteString("# this after confirming which keys can be set in practice.\n\n") 439 printDynamicBlockBody(buf, blockType, &nestedSchema.Block) 440 buf.WriteString("}\n") 441 buf.WriteString("}\n") 442 diags = diags.Append(&hcl2.Diagnostic{ 443 Severity: hcl2.DiagWarning, 444 Summary: "Approximate migration of invalid block type assignment", 445 Detail: fmt.Sprintf("In %s the name %q is a nested block type, but this configuration is exploiting some missing validation rules from Terraform v0.11 and prior to trick Terraform into creating blocks dynamically.\n\nThis has been upgraded to use the new Terraform v0.12 dynamic blocks feature, but since the upgrade tool cannot predict which map keys will be present a fully-comprehensive set has been generated.", blockAddr, blockType), 446 Subject: hcl1PosRange(filename, blockItem.Pos()).Ptr(), 447 }) 448 } 449 } 450 451 return diags 452 } 453 } 454 455 // schemaDefaultBodyRules constructs standard body content rules for the given 456 // schema. Each call is guaranteed to produce a distinct object so that 457 // callers can safely mutate the result in order to impose custom rules 458 // in addition to or instead of those created by default, for situations 459 // where schema-based and predefined items mix in a single body. 460 func schemaDefaultBodyRules(filename string, schema *configschema.Block, an *analysis, adhocComments *commentQueue) bodyContentRules { 461 ret := make(bodyContentRules) 462 if schema == nil { 463 // Shouldn't happen in any real case, but often crops up in tests 464 // where the mock schemas tend to be incomplete. 465 return ret 466 } 467 468 for name, attrS := range schema.Attributes { 469 if aty := attrS.Type; blocktoattr.TypeCanBeBlocks(aty) { 470 // Terraform's standard body processing rules for arbitrary schemas 471 // have a special case where list-of-object or set-of-object 472 // attributes can be specified as a sequence of nested blocks 473 // instead of a single list attribute. We prefer that form during 474 // upgrade for historical reasons, to avoid making large changes 475 // to existing configurations that were following documented idiom. 476 synthSchema := blocktoattr.SchemaForCtyContainerType(aty) 477 nestedRules := schemaDefaultBodyRules(filename, &synthSchema.Block, an, adhocComments) 478 ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, synthSchema, true, an, adhocComments) 479 continue 480 } 481 ret[name] = normalAttributeRule(filename, attrS.Type, an) 482 } 483 for name, blockS := range schema.BlockTypes { 484 nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an, adhocComments) 485 ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, blockS, false, an, adhocComments) 486 } 487 488 return ret 489 } 490 491 // schemaNoInterpBodyRules constructs standard body content rules for the given 492 // schema. Each call is guaranteed to produce a distinct object so that 493 // callers can safely mutate the result in order to impose custom rules 494 // in addition to or instead of those created by default, for situations 495 // where schema-based and predefined items mix in a single body. 496 func schemaNoInterpBodyRules(filename string, schema *configschema.Block, an *analysis, adhocComments *commentQueue) bodyContentRules { 497 ret := make(bodyContentRules) 498 if schema == nil { 499 // Shouldn't happen in any real case, but often crops up in tests 500 // where the mock schemas tend to be incomplete. 501 return ret 502 } 503 504 for name, attrS := range schema.Attributes { 505 ret[name] = noInterpAttributeRule(filename, attrS.Type, an) 506 } 507 for name, blockS := range schema.BlockTypes { 508 nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an, adhocComments) 509 ret[name] = nestedBlockRule(filename, nestedRules, an, adhocComments) 510 } 511 512 return ret 513 } 514 515 // justAttributesBodyRules constructs body content rules that just use the 516 // standard interpolated attribute mapping for every name already present 517 // in the given body object. 518 // 519 // This is a little weird vs. just processing directly the attributes, but 520 // has the advantage that the caller can then apply overrides to the result 521 // as necessary to deal with any known names that need special handling. 522 // 523 // Any attribute rules created by this function do not have a specific wanted 524 // value type specified, instead setting it to just cty.DynamicPseudoType. 525 func justAttributesBodyRules(filename string, body *hcl1ast.ObjectType, an *analysis) bodyContentRules { 526 rules := make(bodyContentRules, len(body.List.Items)) 527 args := body.List.Items 528 for _, arg := range args { 529 name := arg.Keys[0].Token.Value().(string) 530 rules[name] = normalAttributeRule(filename, cty.DynamicPseudoType, an) 531 } 532 return rules 533 } 534 535 func lifecycleBlockBodyRules(filename string, an *analysis) bodyContentRules { 536 return bodyContentRules{ 537 "create_before_destroy": noInterpAttributeRule(filename, cty.Bool, an), 538 "prevent_destroy": noInterpAttributeRule(filename, cty.Bool, an), 539 "ignore_changes": func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 540 var diags tfdiags.Diagnostics 541 val, ok := item.Val.(*hcl1ast.ListType) 542 if !ok { 543 diags = diags.Append(&hcl2.Diagnostic{ 544 Severity: hcl2.DiagError, 545 Summary: "Invalid ignore_changes argument", 546 Detail: `The "ignore_changes" argument must be a list of attribute expressions relative to this resource.`, 547 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 548 }) 549 return diags 550 } 551 552 // As a special case, we'll map the single-element list ["*"] to 553 // the new keyword "all". 554 if len(val.List) == 1 { 555 if lit, ok := val.List[0].(*hcl1ast.LiteralType); ok { 556 if lit.Token.Value() == "*" { 557 printAttribute(buf, item.Keys[0].Token.Value().(string), []byte("all"), item.LineComment) 558 return diags 559 } 560 } 561 } 562 563 var exprBuf bytes.Buffer 564 multiline := len(val.List) > 1 565 exprBuf.WriteByte('[') 566 if multiline { 567 exprBuf.WriteByte('\n') 568 } 569 for _, node := range val.List { 570 itemSrc, moreDiags := upgradeTraversalExpr(node, filename, an) 571 diags = diags.Append(moreDiags) 572 exprBuf.Write(itemSrc) 573 if multiline { 574 exprBuf.WriteString(",\n") 575 } 576 } 577 exprBuf.WriteByte(']') 578 579 printAttribute(buf, item.Keys[0].Token.Value().(string), exprBuf.Bytes(), item.LineComment) 580 581 return diags 582 }, 583 } 584 } 585 586 func provisionerBlockRule(filename string, resourceType string, an *analysis, adhocComments *commentQueue) bodyItemRule { 587 // Unlike some other examples above, this is a rule for the entire 588 // provisioner block, rather than just for its contents. Therefore it must 589 // also produce the block header and body delimiters. 590 return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 591 var diags tfdiags.Diagnostics 592 body := item.Val.(*hcl1ast.ObjectType) 593 declRange := hcl1PosRange(filename, item.Keys[0].Pos()) 594 595 if len(item.Keys) < 2 { 596 diags = diags.Append(&hcl2.Diagnostic{ 597 Severity: hcl2.DiagError, 598 Summary: "Invalid provisioner block", 599 Detail: "A provisioner block must have one label: the provisioner type.", 600 Subject: &declRange, 601 }) 602 return diags 603 } 604 605 typeName := item.Keys[1].Token.Value().(string) 606 schema := an.ProvisionerSchemas[typeName] 607 if schema == nil { 608 // This message is assuming that if the user _is_ using a third-party 609 // provisioner plugin they already know how to install it for normal 610 // use and so we don't need to spell out those instructions in detail 611 // here. 612 diags = diags.Append(&hcl2.Diagnostic{ 613 Severity: hcl2.DiagError, 614 Summary: "Unknown provisioner type", 615 Detail: fmt.Sprintf("The provisioner type %q is not supported. If this is a third-party plugin, make sure its plugin executable is available in one of the usual plugin search paths.", typeName), 616 Subject: &declRange, 617 }) 618 return diags 619 } 620 621 rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) 622 rules["when"] = maybeBareTraversalAttributeRule(filename, an) 623 rules["on_failure"] = maybeBareTraversalAttributeRule(filename, an) 624 rules["connection"] = connectionBlockRule(filename, resourceType, an, adhocComments) 625 626 printComments(buf, item.LeadComment) 627 printBlockOpen(buf, "provisioner", []string{typeName}, item.LineComment) 628 bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("%s.provisioner[%q]", blockAddr, typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments) 629 diags = diags.Append(bodyDiags) 630 buf.WriteString("}\n") 631 632 return diags 633 } 634 } 635 636 func connectionBlockRule(filename string, resourceType string, an *analysis, adhocComments *commentQueue) bodyItemRule { 637 // Unlike some other examples above, this is a rule for the entire 638 // connection block, rather than just for its contents. Therefore it must 639 // also produce the block header and body delimiters. 640 return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 641 var diags tfdiags.Diagnostics 642 body := item.Val.(*hcl1ast.ObjectType) 643 644 // TODO: For the few resource types that were setting ConnInfo in 645 // state after create/update in prior versions, generate the additional 646 // explicit connection settings that are now required if and only if 647 // there's at least one provisioner block. 648 // For now, we just pass this through as-is. 649 650 schema := terraform.ConnectionBlockSupersetSchema() 651 rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) 652 rules["type"] = noInterpAttributeRule(filename, cty.String, an) // type is processed early in the config loader, so cannot interpolate 653 654 printComments(buf, item.LeadComment) 655 printBlockOpen(buf, "connection", nil, item.LineComment) 656 657 // Terraform 0.12 no longer supports "magical" configuration of defaults 658 // in the connection block from logic in the provider because explicit 659 // is better than implicit, but for backward-compatibility we'll populate 660 // an existing connection block with any settings that would've been 661 // previously set automatically for a set of instance types we know 662 // had this behavior in versions prior to the v0.12 release. 663 if defaults := resourceTypeAutomaticConnectionExprs[resourceType]; len(defaults) > 0 { 664 names := make([]string, 0, len(defaults)) 665 for name := range defaults { 666 names = append(names, name) 667 } 668 sort.Strings(names) 669 for _, name := range names { 670 exprSrc := defaults[name] 671 if existing := body.List.Filter(name); len(existing.Items) > 0 { 672 continue // Existing explicit value, so no need for a default 673 } 674 printAttribute(buf, name, []byte(exprSrc), nil) 675 } 676 } 677 678 bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("%s.connection", blockAddr), buf, body.List.Items, body.Rbrace, rules, adhocComments) 679 diags = diags.Append(bodyDiags) 680 buf.WriteString("}\n") 681 682 return diags 683 } 684 } 685 686 func moduleSourceRule(filename string, an *analysis) bodyItemRule { 687 return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 688 var diags tfdiags.Diagnostics 689 val, ok := item.Val.(*hcl1ast.LiteralType) 690 if !ok { 691 diags = diags.Append(&hcl2.Diagnostic{ 692 Severity: hcl2.DiagError, 693 Summary: "Invalid source argument", 694 Detail: `The "source" argument must be a single string containing the module source.`, 695 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 696 }) 697 return diags 698 } 699 if val.Token.Type != hcl1token.STRING { 700 diags = diags.Append(&hcl2.Diagnostic{ 701 Severity: hcl2.DiagError, 702 Summary: "Invalid source argument", 703 Detail: `The "source" argument must be a single string containing the module source.`, 704 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 705 }) 706 return diags 707 } 708 709 litVal := val.Token.Value().(string) 710 711 if isMaybeRelativeLocalPath(litVal, an.ModuleDir) { 712 diags = diags.Append(&hcl2.Diagnostic{ 713 Severity: hcl2.DiagWarning, 714 Summary: "Possible relative module source", 715 Detail: "Terraform cannot determine the given module source, but it appears to be a relative path", 716 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 717 }) 718 buf.WriteString( 719 "# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to\n" + 720 "# reference a relative module source without a preceding ./, but it is no\n" + 721 "# longer supported in Terraform v0.12.\n" + 722 "#\n" + 723 "# If the below module source is indeed a relative local path, add ./ to the\n" + 724 "# start of the source string. If that is not the case, then leave it as-is\n" + 725 "# and remove this TODO comment.\n", 726 ) 727 } 728 newVal, exprDiags := upgradeExpr(val, filename, false, an) 729 diags = diags.Append(exprDiags) 730 buf.WriteString("source = " + string(newVal) + "\n") 731 return diags 732 } 733 } 734 735 // Prior to Terraform 0.12 providers were able to supply default connection 736 // settings that would partially populate the "connection" block with 737 // automatically-selected values. 738 // 739 // In practice, this feature was often confusing in that the provider would not 740 // have enough information to select a suitable host address or protocol from 741 // multiple possible options and so would just make an arbitrary decision. 742 // 743 // With our principle of "explicit is better than implicit", as of Terraform 0.12 744 // we now require all connection settings to be configured explicitly by the 745 // user so that it's clear and explicit in the configuration which protocol and 746 // IP address are being selected. To avoid generating errors immediately after 747 // upgrade, though, we'll make a best effort to populate something functionally 748 // equivalent to what the provider would've done automatically for any resource 749 // types we know about in this table. 750 // 751 // The leaf values in this data structure are raw expressions to be inserted, 752 // and so they must use valid expression syntax as understood by Terraform 0.12. 753 // They should generally be expressions using only constant values or expressions 754 // in terms of attributes accessed via the special "self" object. These should 755 // mimic as closely as possible the logic that the provider itself used to 756 // implement. 757 // 758 // NOTE: Because provider releases are independent from Terraform Core releases, 759 // there could potentially be new 0.11-compatible provider releases that 760 // introduce new uses of default connection info that this map doesn't know 761 // about. The upgrade tool will not handle these, and so we will advise 762 // provider developers that this mechanism is not to be used for any new 763 // resource types, even in 0.11 mode. 764 var resourceTypeAutomaticConnectionExprs = map[string]map[string]string{ 765 "aws_instance": map[string]string{ 766 "type": `"ssh"`, 767 "host": `coalesce(self.public_ip, self.private_ip)`, 768 }, 769 "aws_spot_instance_request": map[string]string{ 770 "type": `"ssh"`, 771 "host": `coalesce(self.public_ip, self.private_ip)`, 772 "user": `self.username != "" ? self.username : null`, 773 "password": `self.password != "" ? self.password : null`, 774 }, 775 "azure_instance": map[string]string{ 776 "type": `"ssh" # TF-UPGRADE-TODO: If this is a windows instance without an SSH server, change to "winrm"`, 777 "host": `coalesce(self.vip_address, self.ip_address)`, 778 }, 779 "azurerm_virtual_machine": map[string]string{ 780 "type": `"ssh" # TF-UPGRADE-TODO: If this is a windows instance without an SSH server, change to "winrm"`, 781 // The azurerm_virtual_machine resource type does not expose a remote 782 // access IP address directly, instead requring the user to separately 783 // fetch the network interface. 784 // (If we can add a "default_ssh_ip" or similar attribute to this 785 // resource type before its first 0.12-compatible release then we should 786 // update this to use that instead, for simplicity's sake.) 787 "host": `"" # TF-UPGRADE-TODO: Set this to the IP address of the machine's primary network interface`, 788 }, 789 "brightbox_server": map[string]string{ 790 "type": `"ssh"`, 791 "host": `coalesce(self.public_hostname, self.ipv6_hostname, self.fqdn)`, 792 }, 793 "cloudscale_server": map[string]string{ 794 "type": `"ssh"`, 795 // The logic for selecting this is pretty complicated for this resource 796 // type, and the result is not exposed as an isolated attribute, so 797 // the conversion here is a little messy. We include newlines in this 798 // one so that the auto-formatter can indent it nicely for readability. 799 // NOTE: In v1.0.1 of this provider (the latest at the time of this 800 // writing) it has an possible bug where it selects _public_ IPv4 801 // addresses but _private_ IPv6 addresses. That behavior is followed 802 // here to maximize compatibility with existing configurations. 803 "host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible. 804 concat( 805 flatten([ 806 for i in self.network_interface : [ 807 for a in i.addresses : a.address 808 if a.version == 4 809 ] 810 if i.type == "public" 811 ]), 812 flatten([ 813 for i in self.network_interface : [ 814 for a in i.addresses : a.address 815 if a.version == 6 816 ] 817 if i.type == "private" 818 ]), 819 )... 820 )`, 821 }, 822 "cloudstack_instance": map[string]string{ 823 "type": `"ssh"`, 824 "host": `self.ip_address`, 825 }, 826 "digitalocean_droplet": map[string]string{ 827 "type": `"ssh"`, 828 "host": `self.ipv4_address`, 829 }, 830 "flexibleengine_compute_bms_server_v2": map[string]string{ 831 "type": `"ssh"`, 832 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 833 }, 834 "flexibleengine_compute_instance_v2": map[string]string{ 835 "type": `"ssh"`, 836 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 837 }, 838 "google_compute_instance": map[string]string{ 839 "type": `"ssh"`, 840 // The logic for selecting this is pretty complicated for this resource 841 // type, and the result is not exposed as an isolated attribute, so 842 // the conversion here is a little messy. We include newlines in this 843 // one so that the auto-formatter can indent it nicely for readability. 844 // (If we can add a "default_ssh_ip" or similar attribute to this 845 // resource type before its first 0.12-compatible release then we should 846 // update this to use that instead, for simplicity's sake.) 847 "host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible. 848 concat( 849 # Prefer any available NAT IP address 850 flatten([ 851 for ni in self.network_interface : [ 852 for ac in ni.access_config : ac.nat_ip 853 ] 854 ]), 855 856 # Otherwise, use the first available LAN IP address 857 [ 858 for ni in self.network_interface : ni.network_ip 859 ], 860 )... 861 )`, 862 }, 863 "hcloud_server": map[string]string{ 864 "type": `"ssh"`, 865 "host": `self.ipv4_address`, 866 }, 867 "huaweicloud_compute_instance_v2": map[string]string{ 868 "type": `"ssh"`, 869 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 870 }, 871 "linode_instance": map[string]string{ 872 "type": `"ssh"`, 873 "host": `self.ipv4[0]`, 874 }, 875 "oneandone_baremetal_server": map[string]string{ 876 "type": `ssh`, 877 "host": `self.ips[0].ip`, 878 "password": `self.password != "" ? self.password : null`, 879 "private_key": `self.ssh_key_path != "" ? file(self.ssh_key_path) : null`, 880 }, 881 "oneandone_server": map[string]string{ 882 "type": `ssh`, 883 "host": `self.ips[0].ip`, 884 "password": `self.password != "" ? self.password : null`, 885 "private_key": `self.ssh_key_path != "" ? file(self.ssh_key_path) : null`, 886 }, 887 "openstack_compute_instance_v2": map[string]string{ 888 "type": `"ssh"`, 889 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 890 }, 891 "opentelekomcloud_compute_bms_server_v2": map[string]string{ 892 "type": `"ssh"`, 893 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 894 }, 895 "opentelekomcloud_compute_instance_v2": map[string]string{ 896 "type": `"ssh"`, 897 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 898 }, 899 "packet_device": map[string]string{ 900 "type": `"ssh"`, 901 "host": `self.access_public_ipv4`, 902 }, 903 "profitbricks_server": map[string]string{ 904 "type": `"ssh"`, 905 "host": `coalesce(self.primary_nic.ips...)`, 906 // The value for this isn't exported anywhere on the object, so we'll 907 // need to have the user fix it up manually. 908 "password": `"" # TF-UPGRADE-TODO: set this to a suitable value, such as the boot image password`, 909 }, 910 "scaleway_server": map[string]string{ 911 "type": `"ssh"`, 912 "host": `self.public_ip`, 913 }, 914 "telefonicaopencloud_compute_bms_server_v2": map[string]string{ 915 "type": `"ssh"`, 916 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 917 }, 918 "telefonicaopencloud_compute_instance_v2": map[string]string{ 919 "type": `"ssh"`, 920 "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, 921 }, 922 "triton_machine": map[string]string{ 923 "type": `"ssh"`, 924 "host": `self.primaryip`, // convention would call for this to be named "primary_ip", but "primaryip" is the name this resource type uses 925 }, 926 "vsphere_virtual_machine": map[string]string{ 927 "type": `"ssh"`, 928 "host": `self.default_ip_address`, 929 }, 930 "yandex_compute_instance": map[string]string{ 931 "type": `"ssh"`, 932 // The logic for selecting this is pretty complicated for this resource 933 // type, and the result is not exposed as an isolated attribute, so 934 // the conversion here is a little messy. We include newlines in this 935 // one so that the auto-formatter can indent it nicely for readability. 936 "host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible. 937 concat( 938 # Prefer any available NAT IP address 939 for i in self.network_interface: [ 940 i.nat_ip_address 941 ], 942 943 # Otherwise, use the first available internal IP address 944 for i in self.network_interface: [ 945 i.ip_address 946 ], 947 )... 948 )`, 949 }, 950 } 951 952 // copied directly from internal/initwd/getter.go 953 var localSourcePrefixes = []string{ 954 "./", 955 "../", 956 ".\\", 957 "..\\", 958 } 959 960 // isMaybeRelativeLocalPath tries to catch situations where a module source is 961 // an improperly-referenced relative path, such as "module" instead of 962 // "./module". This is a simple check that could return a false positive in the 963 // unlikely-yet-plausible case that a module source is for eg. a github 964 // repository that also looks exactly like an existing relative path. This 965 // should only be used to return a warning. 966 func isMaybeRelativeLocalPath(addr, dir string) bool { 967 for _, prefix := range localSourcePrefixes { 968 if strings.HasPrefix(addr, prefix) { 969 // it is _definitely_ a relative path 970 return false 971 } 972 } 973 974 _, err := regsrc.ParseModuleSource(addr) 975 if err == nil { 976 // it is a registry source 977 return false 978 } 979 980 possibleRelPath := filepath.Join(dir, addr) 981 _, err = os.Stat(possibleRelPath) 982 if err == nil { 983 // If there is no error, something exists at what would be the relative 984 // path, if the module source started with ./ 985 return true 986 } 987 988 return false 989 }