github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/upgrade_expr.go (about) 1 package configupgrade 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "strconv" 8 "strings" 9 10 hcl2 "github.com/hashicorp/hcl/v2" 11 hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax" 12 "github.com/zclconf/go-cty/cty" 13 14 hcl1ast "github.com/hashicorp/hcl/hcl/ast" 15 hcl1printer "github.com/hashicorp/hcl/hcl/printer" 16 hcl1token "github.com/hashicorp/hcl/hcl/token" 17 18 "github.com/hashicorp/hil" 19 hilast "github.com/hashicorp/hil/ast" 20 21 "github.com/hashicorp/terraform/addrs" 22 "github.com/hashicorp/terraform/configs/configschema" 23 "github.com/hashicorp/terraform/tfdiags" 24 ) 25 26 func upgradeExpr(val interface{}, filename string, interp bool, an *analysis) ([]byte, tfdiags.Diagnostics) { 27 var buf bytes.Buffer 28 var diags tfdiags.Diagnostics 29 30 // "val" here can be either a hcl1ast.Node or a hilast.Node, since both 31 // of these correspond to expressions in HCL2. Therefore we need to 32 // comprehensively handle every possible HCL1 *and* HIL AST node type 33 // and, at minimum, print it out as-is in HCL2 syntax. 34 Value: 35 switch tv := val.(type) { 36 37 case *hcl1ast.LiteralType: 38 return upgradeExpr(tv.Token, filename, interp, an) 39 40 case hcl1token.Token: 41 switch tv.Type { 42 case hcl1token.STRING: 43 litVal := tv.Value() 44 if !interp { 45 // Easy case, then. 46 printQuotedString(&buf, litVal.(string)) 47 break 48 } 49 50 hilNode, err := hil.Parse(litVal.(string)) 51 if err != nil { 52 diags = diags.Append(&hcl2.Diagnostic{ 53 Severity: hcl2.DiagError, 54 Summary: "Invalid interpolated string", 55 Detail: fmt.Sprintf("Interpolation parsing failed: %s", err), 56 Subject: hcl1PosRange(filename, tv.Pos).Ptr(), 57 }) 58 return nil, diags 59 } 60 61 interpSrc, interpDiags := upgradeExpr(hilNode, filename, interp, an) 62 buf.Write(interpSrc) 63 diags = diags.Append(interpDiags) 64 65 case hcl1token.HEREDOC: 66 // HCL1's "Value" method for tokens pulls out the body and removes 67 // any indents in the source for a flush heredoc, which throws away 68 // information we need to upgrade. Therefore we're going to 69 // re-implement a subset of that logic here where we want to retain 70 // the whitespace verbatim even in flush mode. 71 72 firstNewlineIdx := strings.IndexByte(tv.Text, '\n') 73 if firstNewlineIdx < 0 { 74 // Should never happen, because tv.Value would already have 75 // panicked above in this case. 76 panic("heredoc doesn't contain newline") 77 } 78 introducer := tv.Text[:firstNewlineIdx+1] 79 marker := introducer[2:] // trim off << prefix 80 if marker[0] == '-' { 81 marker = marker[1:] // also trim of - prefix for flush heredoc 82 } 83 body := tv.Text[len(introducer) : len(tv.Text)-len(marker)] 84 flush := introducer[2] == '-' 85 if flush { 86 // HCL1 treats flush heredocs differently, trimming off any 87 // spare whitespace that might appear after the trailing 88 // newline, and so we must replicate that here to avoid 89 // introducing additional whitespace in the output. 90 body = strings.TrimRight(body, " \t") 91 } 92 93 // Now we have: 94 // - introducer is the first line, like "<<-FOO\n" 95 // - marker is the end marker, like "FOO\n" 96 // - body is the raw data between the introducer and the marker, 97 // which we need to do recursive upgrading for. 98 99 buf.WriteString(introducer) 100 if !interp { 101 // Easy case: escape all interpolation-looking sequences. 102 printHeredocLiteralFromHILOutput(&buf, body) 103 } else { 104 hilNode, err := hil.Parse(body) 105 if err != nil { 106 diags = diags.Append(&hcl2.Diagnostic{ 107 Severity: hcl2.DiagError, 108 Summary: "Invalid interpolated string", 109 Detail: fmt.Sprintf("Interpolation parsing failed: %s", err), 110 Subject: hcl1PosRange(filename, tv.Pos).Ptr(), 111 }) 112 } 113 if hilNode != nil { 114 if _, ok := hilNode.(*hilast.Output); !ok { 115 // hil.Parse usually produces an output, but it can sometimes 116 // produce an isolated expression if the input is entirely 117 // a single interpolation. 118 if hilNode != nil { 119 hilNode = &hilast.Output{ 120 Exprs: []hilast.Node{hilNode}, 121 Posx: hilNode.Pos(), 122 } 123 } 124 } 125 interpDiags := upgradeHeredocBody(&buf, hilNode.(*hilast.Output), filename, an) 126 diags = diags.Append(interpDiags) 127 } 128 } 129 if !strings.HasSuffix(body, "\n") { 130 // The versions of HCL1 vendored into Terraform <=0.11 131 // incorrectly allowed the end marker to appear at the end of 132 // the final line of the body, rather than on a line of its own. 133 // That is no longer valid in HCL2, so we need to fix it up. 134 buf.WriteByte('\n') 135 } 136 // NOTE: Marker intentionally contains an extra newline here because 137 // we need to ensure that any follow-on expression bits end up on 138 // a separate line, or else the HCL2 parser won't be able to 139 // recognize the heredoc marker. This causes an extra empty line 140 // in some cases, which we accept for simplicity's sake. 141 buf.WriteString(marker) 142 143 case hcl1token.BOOL: 144 litVal := tv.Value() 145 if litVal.(bool) { 146 buf.WriteString("true") 147 } else { 148 buf.WriteString("false") 149 } 150 151 case hcl1token.NUMBER: 152 num, err := strconv.ParseInt(tv.Text, 0, 64) 153 if err != nil { 154 diags = diags.Append(&hcl2.Diagnostic{ 155 Severity: hcl2.DiagError, 156 Summary: "Invalid number value", 157 Detail: fmt.Sprintf("Parsing failed: %s", err), 158 Subject: hcl1PosRange(filename, tv.Pos).Ptr(), 159 }) 160 } 161 buf.WriteString(strconv.FormatInt(num, 10)) 162 163 case hcl1token.FLOAT: 164 num, err := strconv.ParseFloat(tv.Text, 64) 165 if err != nil { 166 diags = diags.Append(&hcl2.Diagnostic{ 167 Severity: hcl2.DiagError, 168 Summary: "Invalid float value", 169 Detail: fmt.Sprintf("Parsing failed: %s", err), 170 Subject: hcl1PosRange(filename, tv.Pos).Ptr(), 171 }) 172 } 173 buf.WriteString(strconv.FormatFloat(num, 'f', -1, 64)) 174 175 default: 176 // For everything else we'll just pass through the given bytes verbatim, 177 // but we should't get here because the above is intended to be exhaustive. 178 buf.WriteString(tv.Text) 179 180 } 181 182 case *hcl1ast.ListType: 183 multiline := tv.Lbrack.Line != tv.Rbrack.Line 184 buf.WriteString("[") 185 if multiline { 186 buf.WriteString("\n") 187 } 188 for i, node := range tv.List { 189 src, moreDiags := upgradeExpr(node, filename, interp, an) 190 diags = diags.Append(moreDiags) 191 buf.Write(src) 192 if lit, ok := node.(*hcl1ast.LiteralType); ok && lit.LineComment != nil { 193 for _, comment := range lit.LineComment.List { 194 buf.WriteString(", " + comment.Text) 195 buf.WriteString("\n") 196 } 197 } else { 198 if multiline { 199 buf.WriteString(",\n") 200 } else if i < len(tv.List)-1 { 201 buf.WriteString(", ") 202 } 203 } 204 } 205 buf.WriteString("]") 206 207 case *hcl1ast.ObjectType: 208 if len(tv.List.Items) == 0 { 209 buf.WriteString("{}") 210 break 211 } 212 buf.WriteString("{\n") 213 for _, item := range tv.List.Items { 214 if len(item.Keys) != 1 { 215 diags = diags.Append(&hcl2.Diagnostic{ 216 Severity: hcl2.DiagError, 217 Summary: "Invalid map element", 218 Detail: "A map element may not have any block-style labels.", 219 Subject: hcl1PosRange(filename, item.Pos()).Ptr(), 220 }) 221 continue 222 } 223 keySrc, moreDiags := upgradeExpr(item.Keys[0].Token, filename, interp, an) 224 diags = diags.Append(moreDiags) 225 valueSrc, moreDiags := upgradeExpr(item.Val, filename, interp, an) 226 diags = diags.Append(moreDiags) 227 if item.LeadComment != nil { 228 for _, c := range item.LeadComment.List { 229 buf.WriteString(c.Text) 230 buf.WriteByte('\n') 231 } 232 } 233 234 buf.Write(keySrc) 235 buf.WriteString(" = ") 236 buf.Write(valueSrc) 237 if item.LineComment != nil { 238 for _, c := range item.LineComment.List { 239 buf.WriteByte(' ') 240 buf.WriteString(c.Text) 241 } 242 } 243 buf.WriteString("\n") 244 } 245 buf.WriteString("}") 246 247 case hcl1ast.Node: 248 // If our more-specific cases above didn't match this then we'll 249 // ask the hcl1printer package to print the expression out 250 // itself, and assume it'll still be valid in HCL2. 251 // (We should rarely end up here, since our cases above should 252 // be comprehensive.) 253 log.Printf("[TRACE] configupgrade: Don't know how to upgrade %T as expression, so just passing it through as-is", tv) 254 hcl1printer.Fprint(&buf, tv) 255 256 case *hilast.LiteralNode: 257 switch tl := tv.Value.(type) { 258 case string: 259 // This shouldn't generally happen because literal strings are 260 // always wrapped in hilast.Output in HIL, but we'll allow it anyway. 261 printQuotedString(&buf, tl) 262 case int: 263 buf.WriteString(strconv.Itoa(tl)) 264 case float64: 265 buf.WriteString(strconv.FormatFloat(tl, 'f', -1, 64)) 266 case bool: 267 if tl { 268 buf.WriteString("true") 269 } else { 270 buf.WriteString("false") 271 } 272 } 273 274 case *hilast.VariableAccess: 275 // In HIL a variable access is just a single string which might contain 276 // a mixture of identifiers, dots, integer indices, and splat expressions. 277 // All of these concepts were formerly interpreted by Terraform itself, 278 // rather than by HIL. We're going to process this one chunk at a time 279 // here so we can normalize and introduce some newer syntax where it's 280 // safe to do so. 281 parts := strings.Split(tv.Name, ".") 282 283 transformed := transformCountPseudoAttribute(&buf, parts, an) 284 if transformed { 285 break Value 286 } 287 288 parts = upgradeTraversalParts(parts, an) // might add/remove/change parts 289 290 vDiags := validateHilAddress(tv.Name, filename) 291 if len(vDiags) > 0 { 292 diags = diags.Append(vDiags) 293 break 294 } 295 296 printHilTraversalPartsAsHcl2(&buf, parts) 297 298 case *hilast.Arithmetic: 299 op, exists := hilArithmeticOpSyms[tv.Op] 300 if !exists { 301 panic(fmt.Errorf("arithmetic node with unsupported operator %#v", tv.Op)) 302 } 303 304 lhsExpr := tv.Exprs[0] 305 rhsExpr := tv.Exprs[1] 306 lhsSrc, exprDiags := upgradeExpr(lhsExpr, filename, true, an) 307 diags = diags.Append(exprDiags) 308 rhsSrc, exprDiags := upgradeExpr(rhsExpr, filename, true, an) 309 diags = diags.Append(exprDiags) 310 311 // HIL's AST represents -foo as (0 - foo), so we'll recognize 312 // that here and normalize it back. 313 if tv.Op == hilast.ArithmeticOpSub && len(lhsSrc) == 1 && lhsSrc[0] == '0' { 314 buf.WriteString("-") 315 buf.Write(rhsSrc) 316 break 317 } 318 319 buf.Write(lhsSrc) 320 buf.WriteString(op) 321 buf.Write(rhsSrc) 322 323 case *hilast.Call: 324 name := tv.Func 325 args := tv.Args 326 327 // Some adaptations must happen prior to upgrading the arguments, 328 // because they depend on the original argument AST nodes. 329 switch name { 330 case "base64sha256", "base64sha512", "md5", "sha1", "sha256", "sha512": 331 // These functions were sometimes used in conjunction with the 332 // file() function to take the hash of the contents of a file. 333 // Prior to Terraform 0.11 there was a chance of silent corruption 334 // of strings containing non-UTF8 byte sequences, and so we have 335 // made it illegal to use file() with non-text files in 0.12 even 336 // though in this _particular_ situation (passing the function 337 // result directly to another function) there would not be any 338 // corruption; the general rule keeps things consistent. 339 // However, to still meet those use-cases we now have variants of 340 // the hashing functions that have a "file" prefix on their names 341 // and read the contents of a given file, rather than hashing 342 // directly the given string. 343 if len(args) > 0 { 344 if subCall, ok := args[0].(*hilast.Call); ok && subCall.Func == "file" { 345 // We're going to flatten this down into a single call, so 346 // we actually want the arguments of the sub-call here. 347 name = "file" + name 348 args = subCall.Args 349 350 // For this one, we'll fall through to the normal upgrade 351 // handling now that we've fixed up the name and args... 352 } 353 } 354 355 } 356 357 argExprs := make([][]byte, len(args)) 358 multiline := false 359 totalLen := 0 360 for i, arg := range args { 361 if i > 0 { 362 totalLen += 2 363 } 364 exprSrc, exprDiags := upgradeExpr(arg, filename, true, an) 365 diags = diags.Append(exprDiags) 366 argExprs[i] = exprSrc 367 if bytes.Contains(exprSrc, []byte{'\n'}) { 368 // If any of our arguments are multi-line then we'll also be multiline 369 multiline = true 370 } 371 totalLen += len(exprSrc) 372 } 373 374 if totalLen > 60 { // heuristic, since we don't know here how indented we are already 375 multiline = true 376 } 377 378 // Some functions are now better expressed as native language constructs. 379 // These cases will return early if they emit anything, or otherwise 380 // fall through to the default emitter. 381 switch name { 382 case "list": 383 // Should now use tuple constructor syntax 384 buf.WriteByte('[') 385 if multiline { 386 buf.WriteByte('\n') 387 } 388 for i, exprSrc := range argExprs { 389 buf.Write(exprSrc) 390 if multiline { 391 buf.WriteString(",\n") 392 } else { 393 if i < len(args)-1 { 394 buf.WriteString(", ") 395 } 396 } 397 } 398 buf.WriteByte(']') 399 break Value 400 case "map": 401 // Should now use object constructor syntax, but we can only 402 // achieve that if the call is valid, which requires an even 403 // number of arguments. 404 if len(argExprs) == 0 { 405 buf.WriteString("{}") 406 break Value 407 } else if len(argExprs)%2 == 0 { 408 buf.WriteString("{\n") 409 for i := 0; i < len(argExprs); i += 2 { 410 k := argExprs[i] 411 v := argExprs[i+1] 412 413 buf.Write(k) 414 buf.WriteString(" = ") 415 buf.Write(v) 416 buf.WriteByte('\n') 417 } 418 buf.WriteByte('}') 419 break Value 420 } 421 case "lookup": 422 // A lookup call with only two arguments is equivalent to native 423 // index syntax. (A third argument would specify a default value, 424 // so calls like that must be left alone.) 425 // (Note that we can't safely do this for element(...) because 426 // the user may be relying on its wraparound behavior.) 427 if len(argExprs) == 2 { 428 buf.Write(argExprs[0]) 429 buf.WriteByte('[') 430 buf.Write(argExprs[1]) 431 buf.WriteByte(']') 432 break Value 433 } 434 case "element": 435 // We cannot replace element with index syntax safely in general 436 // because users may be relying on its special modulo wraparound 437 // behavior that the index syntax doesn't do. However, if it seems 438 // like the user is trying to use element with a set, we'll insert 439 // an explicit conversion to list to mimic the implicit conversion 440 // that we used to do as an unintended side-effect of how functions 441 // work in HIL. 442 if len(argExprs) > 0 { 443 argTy := an.InferExpressionType(argExprs[0], nil) 444 if argTy.IsSetType() { 445 newExpr := []byte(`tolist(`) 446 newExpr = append(newExpr, argExprs[0]...) 447 newExpr = append(newExpr, ')') 448 argExprs[0] = newExpr 449 } 450 } 451 452 // HIL used some undocumented special functions to implement certain 453 // operations, but since those were actually callable in real expressions 454 // some users inevitably depended on them, so we'll fix them up here. 455 // These each become two function calls to preserve the old behavior 456 // of implicitly converting to the source type first. Usage of these 457 // is relatively rare, so the result doesn't need to be too pretty. 458 case "__builtin_BoolToString": 459 buf.WriteString("tostring(tobool(") 460 buf.Write(argExprs[0]) 461 buf.WriteString("))") 462 break Value 463 case "__builtin_FloatToString": 464 buf.WriteString("tostring(tonumber(") 465 buf.Write(argExprs[0]) 466 buf.WriteString("))") 467 break Value 468 case "__builtin_IntToString": 469 buf.WriteString("tostring(floor(") 470 buf.Write(argExprs[0]) 471 buf.WriteString("))") 472 break Value 473 case "__builtin_StringToInt": 474 buf.WriteString("floor(tostring(") 475 buf.Write(argExprs[0]) 476 buf.WriteString("))") 477 break Value 478 case "__builtin_StringToFloat": 479 buf.WriteString("tonumber(tostring(") 480 buf.Write(argExprs[0]) 481 buf.WriteString("))") 482 break Value 483 case "__builtin_StringToBool": 484 buf.WriteString("tobool(tostring(") 485 buf.Write(argExprs[0]) 486 buf.WriteString("))") 487 break Value 488 case "__builtin_FloatToInt", "__builtin_IntToFloat": 489 // Since "floor" already has an implicit conversion of its argument 490 // to number, and the result is a whole number in either case, 491 // these ones are easier. (We no longer distinguish int and float 492 // as types in HCL2, even though HIL did.) 493 name = "floor" 494 } 495 496 buf.WriteString(name) 497 buf.WriteByte('(') 498 if multiline { 499 buf.WriteByte('\n') 500 } 501 for i, exprSrc := range argExprs { 502 buf.Write(exprSrc) 503 if multiline { 504 buf.WriteString(",\n") 505 } else { 506 if i < len(args)-1 { 507 buf.WriteString(", ") 508 } 509 } 510 } 511 buf.WriteByte(')') 512 513 case *hilast.Conditional: 514 condSrc, exprDiags := upgradeExpr(tv.CondExpr, filename, true, an) 515 diags = diags.Append(exprDiags) 516 trueSrc, exprDiags := upgradeExpr(tv.TrueExpr, filename, true, an) 517 diags = diags.Append(exprDiags) 518 falseSrc, exprDiags := upgradeExpr(tv.FalseExpr, filename, true, an) 519 diags = diags.Append(exprDiags) 520 521 buf.Write(condSrc) 522 buf.WriteString(" ? ") 523 buf.Write(trueSrc) 524 buf.WriteString(" : ") 525 buf.Write(falseSrc) 526 527 case *hilast.Index: 528 target, ok := tv.Target.(*hilast.VariableAccess) 529 if !ok { 530 panic(fmt.Sprintf("Index node with unsupported target type (%T)", tv.Target)) 531 } 532 parts := strings.Split(target.Name, ".") 533 534 keySrc, exprDiags := upgradeExpr(tv.Key, filename, true, an) 535 diags = diags.Append(exprDiags) 536 537 transformed := transformCountPseudoAttribute(&buf, parts, an) 538 if transformed { 539 break Value 540 } 541 542 parts = upgradeTraversalParts(parts, an) // might add/remove/change parts 543 544 vDiags := validateHilAddress(target.Name, filename) 545 if len(vDiags) > 0 { 546 diags = diags.Append(vDiags) 547 break 548 } 549 550 first, remain := parts[0], parts[1:] 551 552 var rAddr addrs.Resource 553 switch parts[0] { 554 case "data": 555 if len(parts) == 5 && parts[3] == "*" { 556 rAddr.Mode = addrs.DataResourceMode 557 rAddr.Type = parts[1] 558 rAddr.Name = parts[2] 559 } 560 default: 561 if len(parts) == 4 && parts[2] == "*" { 562 rAddr.Mode = addrs.ManagedResourceMode 563 rAddr.Type = parts[0] 564 rAddr.Name = parts[1] 565 } 566 } 567 568 // We need to check if the thing being referenced has count 569 // to retain backward compatibility 570 hasCount := false 571 if v, exists := an.ResourceHasCount[rAddr]; exists { 572 hasCount = v 573 } 574 575 hasSplat := false 576 577 buf.WriteString(first) 578 for _, part := range remain { 579 // Attempt to convert old-style splat indexing to new one 580 // e.g. res.label.*.attr[idx] to res.label[idx].attr 581 if part == "*" && hasCount { 582 hasSplat = true 583 buf.WriteString(fmt.Sprintf("[%s]", keySrc)) 584 continue 585 } 586 587 buf.WriteByte('.') 588 buf.WriteString(part) 589 } 590 591 if !hasSplat { 592 buf.WriteString("[") 593 buf.Write(keySrc) 594 buf.WriteString("]") 595 } 596 597 case *hilast.Output: 598 if len(tv.Exprs) == 1 { 599 item := tv.Exprs[0] 600 naked := true 601 if lit, ok := item.(*hilast.LiteralNode); ok { 602 if _, ok := lit.Value.(string); ok { 603 naked = false 604 } 605 } 606 if naked { 607 // If there's only one expression and it isn't a literal string 608 // then we'll just output it naked, since wrapping a single 609 // expression in interpolation is no longer idiomatic. 610 interped, interpDiags := upgradeExpr(item, filename, true, an) 611 diags = diags.Append(interpDiags) 612 buf.Write(interped) 613 break 614 } 615 } 616 617 buf.WriteString(`"`) 618 for _, item := range tv.Exprs { 619 if lit, ok := item.(*hilast.LiteralNode); ok { 620 if litStr, ok := lit.Value.(string); ok { 621 printStringLiteralFromHILOutput(&buf, litStr) 622 continue 623 } 624 } 625 626 interped, interpDiags := upgradeExpr(item, filename, true, an) 627 diags = diags.Append(interpDiags) 628 629 buf.WriteString("${") 630 buf.Write(interped) 631 buf.WriteString("}") 632 } 633 buf.WriteString(`"`) 634 635 case hilast.Node: 636 // Nothing reasonable we can do here, so we should've handled all of 637 // the possibilities above. 638 panic(fmt.Errorf("upgradeExpr doesn't handle HIL node type %T", tv)) 639 640 default: 641 // If we end up in here then the caller gave us something completely invalid. 642 panic(fmt.Errorf("upgradeExpr on unsupported type %T", val)) 643 644 } 645 646 return buf.Bytes(), diags 647 } 648 649 func validateHilAddress(address, filename string) tfdiags.Diagnostics { 650 parts := strings.Split(address, ".") 651 var diags tfdiags.Diagnostics 652 653 label, ok := getResourceLabel(parts) 654 if ok && !hcl2syntax.ValidIdentifier(label) { 655 // We can't get any useful source location out of HIL unfortunately 656 diags = diags.Append(tfdiags.Sourceless( 657 tfdiags.Error, 658 fmt.Sprintf("Invalid address (%s) in ./%s", address, filename), 659 // The label could be invalid for another reason 660 // but this is the most likely, so we add it as hint 661 "Names of objects (resources, modules, etc) may no longer start with digits.")) 662 } 663 664 return diags 665 } 666 667 func getResourceLabel(parts []string) (string, bool) { 668 if len(parts) < 1 { 669 return "", false 670 } 671 672 if parts[0] == "data" { 673 if len(parts) < 3 { 674 return "", false 675 } 676 return parts[2], true 677 } 678 679 if len(parts) < 2 { 680 return "", false 681 } 682 683 return parts[1], true 684 } 685 686 // transformCountPseudoAttribute deals with the .count pseudo-attributes 687 // that 0.11 and prior allowed for resources. These no longer exist, 688 // because they don't do anything we can't do with the length(...) function. 689 func transformCountPseudoAttribute(buf *bytes.Buffer, parts []string, an *analysis) (transformed bool) { 690 if len(parts) > 0 { 691 var rAddr addrs.Resource 692 switch parts[0] { 693 case "data": 694 if len(parts) == 4 && parts[3] == "count" { 695 rAddr.Mode = addrs.DataResourceMode 696 rAddr.Type = parts[1] 697 rAddr.Name = parts[2] 698 } 699 default: 700 if len(parts) == 3 && parts[2] == "count" { 701 rAddr.Mode = addrs.ManagedResourceMode 702 rAddr.Type = parts[0] 703 rAddr.Name = parts[1] 704 } 705 } 706 // We need to check if the thing being referenced is actually an 707 // existing resource, because other three-part traversals might 708 // coincidentally end with "count". 709 if hasCount, exists := an.ResourceHasCount[rAddr]; exists { 710 if hasCount { 711 buf.WriteString("length(") 712 buf.WriteString(rAddr.String()) 713 buf.WriteString(")") 714 } else { 715 // If the resource does not have count, the .count 716 // attr would've always returned 1 before. 717 buf.WriteString("1") 718 } 719 transformed = true 720 return 721 } 722 } 723 return 724 } 725 726 func printHilTraversalPartsAsHcl2(buf *bytes.Buffer, parts []string) { 727 first, remain := parts[0], parts[1:] 728 buf.WriteString(first) 729 seenSplat := false 730 for _, part := range remain { 731 if part == "*" { 732 seenSplat = true 733 buf.WriteString(".*") 734 continue 735 } 736 737 // Other special cases apply only if we've not previously 738 // seen a splat expression marker, since attribute vs. index 739 // syntax have different interpretations after a simple splat. 740 if !seenSplat { 741 if v, err := strconv.Atoi(part); err == nil { 742 // Looks like it's old-style index traversal syntax foo.0.bar 743 // so we'll replace with canonical index syntax foo[0].bar. 744 fmt.Fprintf(buf, "[%d]", v) 745 continue 746 } 747 if !hcl2syntax.ValidIdentifier(part) { 748 // This should be rare since HIL's identifier syntax is _close_ 749 // to HCL2's, but we'll get here if one of the intervening 750 // parts is not a valid identifier in isolation, since HIL 751 // did not consider these to be separate identifiers. 752 // e.g. foo.1bar would be invalid in HCL2; must instead be foo["1bar"]. 753 buf.WriteByte('[') 754 printQuotedString(buf, part) 755 buf.WriteByte(']') 756 continue 757 } 758 } 759 760 buf.WriteByte('.') 761 buf.WriteString(part) 762 } 763 } 764 765 func upgradeHeredocBody(buf *bytes.Buffer, val *hilast.Output, filename string, an *analysis) tfdiags.Diagnostics { 766 var diags tfdiags.Diagnostics 767 768 for _, item := range val.Exprs { 769 if lit, ok := item.(*hilast.LiteralNode); ok { 770 if litStr, ok := lit.Value.(string); ok { 771 printHeredocLiteralFromHILOutput(buf, litStr) 772 continue 773 } 774 } 775 interped, interpDiags := upgradeExpr(item, filename, true, an) 776 diags = diags.Append(interpDiags) 777 778 buf.WriteString("${") 779 buf.Write(interped) 780 buf.WriteString("}") 781 } 782 783 return diags 784 } 785 786 func upgradeTraversalExpr(val interface{}, filename string, an *analysis) ([]byte, tfdiags.Diagnostics) { 787 if lit, ok := val.(*hcl1ast.LiteralType); ok && lit.Token.Type == hcl1token.STRING { 788 trStr := lit.Token.Value().(string) 789 if strings.HasSuffix(trStr, ".%") || strings.HasSuffix(trStr, ".#") { 790 // Terraform 0.11 would often not validate traversals given in 791 // strings and so users would get away with this sort of 792 // flatmap-implementation-detail reference, particularly inside 793 // ignore_changes. We'll just trim these off to tolerate it, 794 // rather than failing below in ParseTraversalAbs. 795 trStr = trStr[:len(trStr)-2] 796 } 797 trSrc := []byte(trStr) 798 _, trDiags := hcl2syntax.ParseTraversalAbs(trSrc, "", hcl2.Pos{}) 799 if !trDiags.HasErrors() { 800 return trSrc, nil 801 } 802 } 803 return upgradeExpr(val, filename, false, an) 804 } 805 806 var hilArithmeticOpSyms = map[hilast.ArithmeticOp]string{ 807 hilast.ArithmeticOpAdd: " + ", 808 hilast.ArithmeticOpSub: " - ", 809 hilast.ArithmeticOpMul: " * ", 810 hilast.ArithmeticOpDiv: " / ", 811 hilast.ArithmeticOpMod: " % ", 812 813 hilast.ArithmeticOpLogicalAnd: " && ", 814 hilast.ArithmeticOpLogicalOr: " || ", 815 816 hilast.ArithmeticOpEqual: " == ", 817 hilast.ArithmeticOpNotEqual: " != ", 818 hilast.ArithmeticOpLessThan: " < ", 819 hilast.ArithmeticOpLessThanOrEqual: " <= ", 820 hilast.ArithmeticOpGreaterThan: " > ", 821 hilast.ArithmeticOpGreaterThanOrEqual: " >= ", 822 } 823 824 // upgradeTraversalParts might alter the given split parts from a HIL-style 825 // variable access to account for renamings made in Terraform v0.12. 826 func upgradeTraversalParts(parts []string, an *analysis) []string { 827 parts = upgradeCountTraversalParts(parts, an) 828 parts = upgradeTerraformRemoteStateTraversalParts(parts, an) 829 return parts 830 } 831 832 func upgradeCountTraversalParts(parts []string, an *analysis) []string { 833 // test_instance.foo.id needs to become test_instance.foo[0].id if 834 // count is set for test_instance.foo. Likewise, if count _isn't_ set 835 // then test_instance.foo.0.id must become test_instance.foo.id. 836 if len(parts) < 3 { 837 return parts 838 } 839 var addr addrs.Resource 840 var idxIdx int 841 switch parts[0] { 842 case "data": 843 addr.Mode = addrs.DataResourceMode 844 addr.Type = parts[1] 845 addr.Name = parts[2] 846 idxIdx = 3 847 default: 848 addr.Mode = addrs.ManagedResourceMode 849 addr.Type = parts[0] 850 addr.Name = parts[1] 851 idxIdx = 2 852 } 853 854 hasCount, exists := an.ResourceHasCount[addr] 855 if !exists { 856 // Probably not actually a resource instance at all, then. 857 return parts 858 } 859 860 // Since at least one attribute is required after a resource reference 861 // prior to Terraform v0.12, we can assume there will be at least enough 862 // parts to contain the index even if no index is actually present. 863 if idxIdx >= len(parts) { 864 return parts 865 } 866 867 maybeIdx := parts[idxIdx] 868 switch { 869 case hasCount: 870 if _, err := strconv.Atoi(maybeIdx); err == nil || maybeIdx == "*" { 871 // Has an index already, so no changes required. 872 return parts 873 } 874 // Need to insert index zero at idxIdx. 875 log.Printf("[TRACE] configupgrade: %s has count but reference does not have index, so adding one", addr) 876 newParts := make([]string, len(parts)+1) 877 copy(newParts, parts[:idxIdx]) 878 newParts[idxIdx] = "0" 879 copy(newParts[idxIdx+1:], parts[idxIdx:]) 880 return newParts 881 default: 882 // For removing indexes we'll be more conservative and only remove 883 // exactly index "0", because other indexes on a resource without 884 // count are invalid anyway and we're better off letting the normal 885 // configuration parser deal with that. 886 if maybeIdx != "0" { 887 return parts 888 } 889 890 // Need to remove the index zero. 891 log.Printf("[TRACE] configupgrade: %s does not have count but reference has index, so removing it", addr) 892 newParts := make([]string, len(parts)-1) 893 copy(newParts, parts[:idxIdx]) 894 copy(newParts[idxIdx:], parts[idxIdx+1:]) 895 return newParts 896 } 897 } 898 899 func upgradeTerraformRemoteStateTraversalParts(parts []string, an *analysis) []string { 900 // data.terraform_remote_state.x.foo needs to become 901 // data.terraform_remote_state.x.outputs.foo unless "foo" is a real 902 // attribute in the object type implied by the remote state schema. 903 if len(parts) < 4 { 904 return parts 905 } 906 if parts[0] != "data" || parts[1] != "terraform_remote_state" { 907 return parts 908 } 909 910 attrIdx := 3 911 if parts[attrIdx] == "*" { 912 attrIdx = 4 // data.terraform_remote_state.x.*.foo 913 } else if _, err := strconv.Atoi(parts[attrIdx]); err == nil { 914 attrIdx = 4 // data.terraform_remote_state.x.1.foo 915 } 916 if attrIdx >= len(parts) { 917 return parts 918 } 919 920 attrName := parts[attrIdx] 921 922 // Now we'll use the schema of data.terraform_remote_state to decide if 923 // the user intended this to be an output, or whether it's one of the real 924 // attributes of this data source. 925 var schema *configschema.Block 926 if providerSchema := an.ProviderSchemas["terraform"]; providerSchema != nil { 927 schema, _ = providerSchema.SchemaForResourceType(addrs.DataResourceMode, "terraform_remote_state") 928 } 929 // Schema should be available in all reasonable cases, but might be nil 930 // if input configuration contains a reference to a remote state data resource 931 // without actually defining that data resource. In that weird edge case, 932 // we'll just assume all attributes are outputs. 933 if schema != nil && schema.ImpliedType().HasAttribute(attrName) { 934 // User is accessing one of the real attributes, then, and we have 935 // no need to rewrite it. 936 return parts 937 } 938 939 // If we get down here then our task is to produce a new parts slice 940 // that has the fixed additional attribute name "outputs" inserted at 941 // attrIdx, retaining all other parts. 942 newParts := make([]string, len(parts)+1) 943 copy(newParts, parts[:attrIdx]) 944 newParts[attrIdx] = "outputs" 945 copy(newParts[attrIdx+1:], parts[attrIdx:]) 946 return newParts 947 } 948 949 func typeIsSettableFromTupleCons(ty cty.Type) bool { 950 return ty.IsListType() || ty.IsTupleType() || ty.IsSetType() 951 }