github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/upgrade_native.go (about) 1 package configupgrade 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "log" 8 "regexp" 9 "sort" 10 "strings" 11 12 version "github.com/hashicorp/go-version" 13 14 hcl1ast "github.com/hashicorp/hcl/hcl/ast" 15 hcl1parser "github.com/hashicorp/hcl/hcl/parser" 16 hcl1printer "github.com/hashicorp/hcl/hcl/printer" 17 hcl1token "github.com/hashicorp/hcl/hcl/token" 18 19 hcl2 "github.com/hashicorp/hcl/v2" 20 "github.com/zclconf/go-cty/cty" 21 22 "github.com/hashicorp/terraform/addrs" 23 backendinit "github.com/hashicorp/terraform/backend/init" 24 "github.com/hashicorp/terraform/configs/configschema" 25 "github.com/hashicorp/terraform/tfdiags" 26 ) 27 28 type upgradeFileResult struct { 29 Content []byte 30 ProviderRequirements map[string]version.Constraints 31 } 32 33 func (u *Upgrader) upgradeNativeSyntaxFile(filename string, src []byte, an *analysis) (upgradeFileResult, tfdiags.Diagnostics) { 34 var result upgradeFileResult 35 var diags tfdiags.Diagnostics 36 37 log.Printf("[TRACE] configupgrade: Working on %q", filename) 38 39 var buf bytes.Buffer 40 41 f, err := hcl1parser.Parse(src) 42 if err != nil { 43 return result, diags.Append(&hcl2.Diagnostic{ 44 Severity: hcl2.DiagError, 45 Summary: "Syntax error in configuration file", 46 Detail: fmt.Sprintf("Error while parsing: %s", err), 47 Subject: hcl1ErrSubjectRange(filename, err), 48 }) 49 } 50 51 rootList := f.Node.(*hcl1ast.ObjectList) 52 rootItems := rootList.Items 53 adhocComments := collectAdhocComments(f) 54 55 for _, item := range rootItems { 56 comments := adhocComments.TakeBefore(item) 57 for _, group := range comments { 58 printComments(&buf, group) 59 buf.WriteByte('\n') // Extra separator after each group 60 } 61 62 blockType := item.Keys[0].Token.Value().(string) 63 labels := make([]string, len(item.Keys)-1) 64 for i, key := range item.Keys[1:] { 65 labels[i] = key.Token.Value().(string) 66 } 67 body, isObject := item.Val.(*hcl1ast.ObjectType) 68 if !isObject { 69 // Should never happen for valid input, since we don't expect 70 // any non-block items at our top level. 71 diags = diags.Append(&hcl2.Diagnostic{ 72 Severity: hcl2.DiagWarning, 73 Summary: "Unsupported top-level attribute", 74 Detail: fmt.Sprintf("Attribute %q is not expected here, so its expression was not upgraded.", blockType), 75 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 76 }) 77 // Preserve the item as-is, using the hcl1printer package. 78 buf.WriteString("# TF-UPGRADE-TODO: Top-level attributes are not valid, so this was not automatically upgraded.\n") 79 hcl1printer.Fprint(&buf, item) 80 buf.WriteString("\n\n") 81 continue 82 } 83 declRange := hcl1PosRange(filename, item.Keys[0].Pos()) 84 85 switch blockType { 86 87 case "resource", "data": 88 if len(labels) != 2 { 89 // Should never happen for valid input. 90 diags = diags.Append(&hcl2.Diagnostic{ 91 Severity: hcl2.DiagError, 92 Summary: fmt.Sprintf("Invalid %s block", blockType), 93 Detail: fmt.Sprintf("A %s block must have two labels: the type and the name.", blockType), 94 Subject: &declRange, 95 }) 96 continue 97 } 98 99 rAddr := addrs.Resource{ 100 Mode: addrs.ManagedResourceMode, 101 Type: labels[0], 102 Name: labels[1], 103 } 104 if blockType == "data" { 105 rAddr.Mode = addrs.DataResourceMode 106 } 107 108 log.Printf("[TRACE] configupgrade: Upgrading %s at %s", rAddr, declRange) 109 moreDiags := u.upgradeNativeSyntaxResource(filename, &buf, rAddr, item, an, adhocComments) 110 diags = diags.Append(moreDiags) 111 112 case "provider": 113 if len(labels) != 1 { 114 diags = diags.Append(&hcl2.Diagnostic{ 115 Severity: hcl2.DiagError, 116 Summary: fmt.Sprintf("Invalid %s block", blockType), 117 Detail: fmt.Sprintf("A %s block must have one label: the provider type.", blockType), 118 Subject: &declRange, 119 }) 120 continue 121 } 122 123 pType := labels[0] 124 log.Printf("[TRACE] configupgrade: Upgrading provider.%s at %s", pType, declRange) 125 moreDiags := u.upgradeNativeSyntaxProvider(filename, &buf, pType, item, an, adhocComments) 126 diags = diags.Append(moreDiags) 127 128 case "terraform": 129 if len(labels) != 0 { 130 diags = diags.Append(&hcl2.Diagnostic{ 131 Severity: hcl2.DiagError, 132 Summary: fmt.Sprintf("Invalid %s block", blockType), 133 Detail: fmt.Sprintf("A %s block must not have any labels.", blockType), 134 Subject: &declRange, 135 }) 136 continue 137 } 138 moreDiags := u.upgradeNativeSyntaxTerraformBlock(filename, &buf, item, an, adhocComments) 139 diags = diags.Append(moreDiags) 140 141 case "variable": 142 if len(labels) != 1 { 143 diags = diags.Append(&hcl2.Diagnostic{ 144 Severity: hcl2.DiagError, 145 Summary: fmt.Sprintf("Invalid %s block", blockType), 146 Detail: fmt.Sprintf("A %s block must have one label: the variable name.", blockType), 147 Subject: &declRange, 148 }) 149 continue 150 } 151 152 printComments(&buf, item.LeadComment) 153 printBlockOpen(&buf, blockType, labels, item.LineComment) 154 rules := bodyContentRules{ 155 "description": noInterpAttributeRule(filename, cty.String, an), 156 "default": noInterpAttributeRule(filename, cty.DynamicPseudoType, an), 157 "type": maybeBareKeywordAttributeRule(filename, an, map[string]string{ 158 // "list" and "map" in older versions were documented to 159 // mean list and map of strings, so we'll migrate to that 160 // and let the user adjust it to some other type if desired. 161 "list": `list(string)`, 162 "map": `map(string)`, 163 }), 164 } 165 log.Printf("[TRACE] configupgrade: Upgrading var.%s at %s", labels[0], declRange) 166 bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("var.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments) 167 diags = diags.Append(bodyDiags) 168 buf.WriteString("}\n\n") 169 170 case "output": 171 if len(labels) != 1 { 172 diags = diags.Append(&hcl2.Diagnostic{ 173 Severity: hcl2.DiagError, 174 Summary: fmt.Sprintf("Invalid %s block", blockType), 175 Detail: fmt.Sprintf("A %s block must have one label: the output name.", blockType), 176 Subject: &declRange, 177 }) 178 continue 179 } 180 181 printComments(&buf, item.LeadComment) 182 if invalidLabel(labels[0]) { 183 printLabelTodo(&buf, labels[0]) 184 } 185 printBlockOpen(&buf, blockType, labels, item.LineComment) 186 187 rules := bodyContentRules{ 188 "description": noInterpAttributeRule(filename, cty.String, an), 189 "value": normalAttributeRule(filename, cty.DynamicPseudoType, an), 190 "sensitive": noInterpAttributeRule(filename, cty.Bool, an), 191 "depends_on": dependsOnAttributeRule(filename, an), 192 } 193 log.Printf("[TRACE] configupgrade: Upgrading output.%s at %s", labels[0], declRange) 194 bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("output.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments) 195 diags = diags.Append(bodyDiags) 196 buf.WriteString("}\n\n") 197 198 case "module": 199 if len(labels) != 1 { 200 diags = diags.Append(&hcl2.Diagnostic{ 201 Severity: hcl2.DiagError, 202 Summary: fmt.Sprintf("Invalid %s block", blockType), 203 Detail: fmt.Sprintf("A %s block must have one label: the module call name.", blockType), 204 Subject: &declRange, 205 }) 206 continue 207 } 208 209 // Since upgrading is a single-module endeavor, we don't have access 210 // to the configuration of the child module here, but we know that 211 // in practice all arguments that aren't reserved meta-arguments 212 // in a module block are normal expression attributes so we'll 213 // start with the straightforward mapping of those and override 214 // the special lifecycle arguments below. 215 rules := justAttributesBodyRules(filename, body, an) 216 rules["source"] = moduleSourceRule(filename, an) 217 rules["version"] = noInterpAttributeRule(filename, cty.String, an) 218 rules["providers"] = func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 219 var diags tfdiags.Diagnostics 220 subBody, ok := item.Val.(*hcl1ast.ObjectType) 221 if !ok { 222 diags = diags.Append(&hcl2.Diagnostic{ 223 Severity: hcl2.DiagError, 224 Summary: "Invalid providers argument", 225 Detail: `The "providers" argument must be a map from provider addresses in the child module to corresponding provider addresses in this module.`, 226 Subject: &declRange, 227 }) 228 return diags 229 } 230 231 // We're gonna cheat here and use justAttributesBodyRules to 232 // find all the attribute names but then just rewrite them all 233 // to be our specialized traversal-style mapping instead. 234 subRules := justAttributesBodyRules(filename, subBody, an) 235 for k := range subRules { 236 subRules[k] = maybeBareTraversalAttributeRule(filename, an) 237 } 238 buf.WriteString("providers = {\n") 239 bodyDiags := upgradeBlockBody(filename, blockAddr, buf, subBody.List.Items, body.Rbrace, subRules, adhocComments) 240 diags = diags.Append(bodyDiags) 241 buf.WriteString("}\n") 242 243 return diags 244 } 245 246 printComments(&buf, item.LeadComment) 247 printBlockOpen(&buf, blockType, labels, item.LineComment) 248 log.Printf("[TRACE] configupgrade: Upgrading module.%s at %s", labels[0], declRange) 249 bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("module.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments) 250 diags = diags.Append(bodyDiags) 251 buf.WriteString("}\n\n") 252 253 case "locals": 254 log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange) 255 printComments(&buf, item.LeadComment) 256 printBlockOpen(&buf, blockType, labels, item.LineComment) 257 258 // The "locals" block contents are free-form declarations, so 259 // we'll just use the default attribute mapping rule for everything 260 // inside it. 261 rules := justAttributesBodyRules(filename, body, an) 262 log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange) 263 bodyDiags := upgradeBlockBody(filename, "locals", &buf, body.List.Items, body.Rbrace, rules, adhocComments) 264 diags = diags.Append(bodyDiags) 265 buf.WriteString("}\n\n") 266 267 default: 268 // Should never happen for valid input, because the above cases 269 // are exhaustive for valid blocks as of Terraform 0.11. 270 diags = diags.Append(&hcl2.Diagnostic{ 271 Severity: hcl2.DiagWarning, 272 Summary: "Unsupported root block type", 273 Detail: fmt.Sprintf("The block type %q is not expected here, so its content was not upgraded.", blockType), 274 Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), 275 }) 276 277 // Preserve the block as-is, using the hcl1printer package. 278 buf.WriteString("# TF-UPGRADE-TODO: Block type was not recognized, so this block and its contents were not automatically upgraded.\n") 279 hcl1printer.Fprint(&buf, item) 280 buf.WriteString("\n\n") 281 continue 282 } 283 } 284 285 // Print out any leftover comments 286 for _, group := range *adhocComments { 287 printComments(&buf, group) 288 } 289 290 result.Content = buf.Bytes() 291 292 return result, diags 293 } 294 295 func (u *Upgrader) upgradeNativeSyntaxResource(filename string, buf *bytes.Buffer, addr addrs.Resource, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics { 296 var diags tfdiags.Diagnostics 297 298 body := item.Val.(*hcl1ast.ObjectType) 299 declRange := hcl1PosRange(filename, item.Keys[0].Pos()) 300 301 // We should always have a schema for each provider in our analysis 302 // object. If not, it's a bug in the analyzer. 303 providerType, ok := an.ResourceProviderType[addr] 304 if !ok { 305 panic(fmt.Sprintf("unknown provider type for %s", addr.String())) 306 } 307 providerSchema, ok := an.ProviderSchemas[providerType] 308 if !ok { 309 panic(fmt.Sprintf("missing schema for provider type %q", providerType)) 310 } 311 schema, _ := providerSchema.SchemaForResourceAddr(addr) 312 if schema == nil { 313 diags = diags.Append(&hcl2.Diagnostic{ 314 Severity: hcl2.DiagError, 315 Summary: "Unknown resource type", 316 Detail: fmt.Sprintf("The resource type %q is not known to the currently-selected version of provider %q.", addr.Type, providerType), 317 Subject: &declRange, 318 }) 319 return diags 320 } 321 322 var blockType string 323 switch addr.Mode { 324 case addrs.ManagedResourceMode: 325 blockType = "resource" 326 case addrs.DataResourceMode: 327 blockType = "data" 328 } 329 labels := []string{addr.Type, addr.Name} 330 331 rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) 332 rules["count"] = normalAttributeRule(filename, cty.Number, an) 333 rules["depends_on"] = dependsOnAttributeRule(filename, an) 334 rules["provider"] = maybeBareTraversalAttributeRule(filename, an) 335 rules["lifecycle"] = nestedBlockRule(filename, lifecycleBlockBodyRules(filename, an), an, adhocComments) 336 if addr.Mode == addrs.ManagedResourceMode { 337 rules["connection"] = connectionBlockRule(filename, addr.Type, an, adhocComments) 338 rules["provisioner"] = provisionerBlockRule(filename, addr.Type, an, adhocComments) 339 } 340 341 printComments(buf, item.LeadComment) 342 if invalidLabel(labels[1]) { 343 printLabelTodo(buf, labels[1]) 344 } 345 printBlockOpen(buf, blockType, labels, item.LineComment) 346 bodyDiags := upgradeBlockBody(filename, addr.String(), buf, body.List.Items, body.Rbrace, rules, adhocComments) 347 diags = diags.Append(bodyDiags) 348 buf.WriteString("}\n\n") 349 350 return diags 351 } 352 353 func (u *Upgrader) upgradeNativeSyntaxProvider(filename string, buf *bytes.Buffer, typeName string, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics { 354 var diags tfdiags.Diagnostics 355 356 body := item.Val.(*hcl1ast.ObjectType) 357 358 // We should always have a schema for each provider in our analysis 359 // object. If not, it's a bug in the analyzer. 360 providerSchema, ok := an.ProviderSchemas[typeName] 361 if !ok { 362 panic(fmt.Sprintf("missing schema for provider type %q", typeName)) 363 } 364 schema := providerSchema.Provider 365 rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) 366 rules["alias"] = noInterpAttributeRule(filename, cty.String, an) 367 rules["version"] = noInterpAttributeRule(filename, cty.String, an) 368 369 printComments(buf, item.LeadComment) 370 printBlockOpen(buf, "provider", []string{typeName}, item.LineComment) 371 bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("provider.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments) 372 diags = diags.Append(bodyDiags) 373 buf.WriteString("}\n\n") 374 375 return diags 376 } 377 378 func (u *Upgrader) upgradeNativeSyntaxTerraformBlock(filename string, buf *bytes.Buffer, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics { 379 var diags tfdiags.Diagnostics 380 381 body := item.Val.(*hcl1ast.ObjectType) 382 383 rules := bodyContentRules{ 384 "required_version": noInterpAttributeRule(filename, cty.String, an), 385 "backend": func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { 386 var diags tfdiags.Diagnostics 387 388 declRange := hcl1PosRange(filename, item.Keys[0].Pos()) 389 if len(item.Keys) != 2 { 390 diags = diags.Append(&hcl2.Diagnostic{ 391 Severity: hcl2.DiagError, 392 Summary: `Invalid backend block`, 393 Detail: `A backend block must have one label: the backend type name.`, 394 Subject: &declRange, 395 }) 396 return diags 397 } 398 399 typeName := item.Keys[1].Token.Value().(string) 400 beFn := backendinit.Backend(typeName) 401 if beFn == nil { 402 diags = diags.Append(&hcl2.Diagnostic{ 403 Severity: hcl2.DiagError, 404 Summary: "Unsupported backend type", 405 Detail: fmt.Sprintf("Terraform does not support a backend type named %q.", typeName), 406 Subject: &declRange, 407 }) 408 return diags 409 } 410 be := beFn() 411 schema := be.ConfigSchema() 412 rules := schemaNoInterpBodyRules(filename, schema, an, adhocComments) 413 414 body := item.Val.(*hcl1ast.ObjectType) 415 416 printComments(buf, item.LeadComment) 417 printBlockOpen(buf, "backend", []string{typeName}, item.LineComment) 418 bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("terraform.backend.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments) 419 diags = diags.Append(bodyDiags) 420 buf.WriteString("}\n") 421 422 return diags 423 }, 424 } 425 426 printComments(buf, item.LeadComment) 427 printBlockOpen(buf, "terraform", nil, item.LineComment) 428 bodyDiags := upgradeBlockBody(filename, "terraform", buf, body.List.Items, body.Rbrace, rules, adhocComments) 429 diags = diags.Append(bodyDiags) 430 buf.WriteString("}\n\n") 431 432 return diags 433 } 434 435 func upgradeBlockBody(filename string, blockAddr string, buf *bytes.Buffer, args []*hcl1ast.ObjectItem, end hcl1token.Pos, rules bodyContentRules, adhocComments *commentQueue) tfdiags.Diagnostics { 436 var diags tfdiags.Diagnostics 437 438 for i, arg := range args { 439 comments := adhocComments.TakeBefore(arg) 440 for _, group := range comments { 441 printComments(buf, group) 442 buf.WriteByte('\n') // Extra separator after each group 443 } 444 445 printComments(buf, arg.LeadComment) 446 447 name := arg.Keys[0].Token.Value().(string) 448 449 rule, expected := rules[name] 450 if !expected { 451 if arg.Assign.IsValid() { 452 diags = diags.Append(&hcl2.Diagnostic{ 453 Severity: hcl2.DiagError, 454 Summary: "Unrecognized attribute name", 455 Detail: fmt.Sprintf("No attribute named %q is expected in %s.", name, blockAddr), 456 Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(), 457 }) 458 } else { 459 diags = diags.Append(&hcl2.Diagnostic{ 460 Severity: hcl2.DiagError, 461 Summary: "Unrecognized block type", 462 Detail: fmt.Sprintf("Blocks of type %q are not expected in %s.", name, blockAddr), 463 Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(), 464 }) 465 } 466 continue 467 } 468 469 itemDiags := rule(buf, blockAddr, arg) 470 diags = diags.Append(itemDiags) 471 472 // If we have another item and it's more than one line away 473 // from the current one then we'll print an extra blank line 474 // to retain that separation. 475 if (i + 1) < len(args) { 476 next := args[i+1] 477 thisPos := hcl1NodeEndPos(arg) 478 nextPos := next.Pos() 479 if nextPos.Line-thisPos.Line > 1 { 480 buf.WriteByte('\n') 481 } 482 } 483 } 484 485 // Before we return, we must also print any remaining adhocComments that 486 // appear between our last item and the closing brace. 487 comments := adhocComments.TakeBeforePos(end) 488 for i, group := range comments { 489 printComments(buf, group) 490 if i < len(comments)-1 { 491 buf.WriteByte('\n') // Extra separator after each group 492 } 493 } 494 495 return diags 496 } 497 498 // printDynamicBody prints out a conservative, exhaustive dynamic block body 499 // for every attribute and nested block in the given schema, for situations 500 // when a dynamic expression was being assigned to a block type name in input 501 // configuration and so we can assume it's a list of maps but can't make 502 // any assumptions about what subset of the schema-specified keys might be 503 // present in the map values. 504 func printDynamicBlockBody(buf *bytes.Buffer, iterName string, schema *configschema.Block) tfdiags.Diagnostics { 505 var diags tfdiags.Diagnostics 506 507 attrNames := make([]string, 0, len(schema.Attributes)) 508 for name := range schema.Attributes { 509 attrNames = append(attrNames, name) 510 } 511 sort.Strings(attrNames) 512 for _, name := range attrNames { 513 attrS := schema.Attributes[name] 514 if !(attrS.Required || attrS.Optional) { // no Computed-only attributes 515 continue 516 } 517 if attrS.Required { 518 // For required attributes we can generate a simpler expression 519 // that just assumes the presence of the key representing the 520 // attribute value. 521 printAttribute(buf, name, []byte(fmt.Sprintf(`%s.value.%s`, iterName, name)), nil) 522 } else { 523 // Otherwise we must be conservative and generate a conditional 524 // lookup that will just populate nothing at all if the expected 525 // key is not present. 526 printAttribute(buf, name, []byte(fmt.Sprintf(`lookup(%s.value, %q, null)`, iterName, name)), nil) 527 } 528 } 529 530 blockTypeNames := make([]string, 0, len(schema.BlockTypes)) 531 for name := range schema.BlockTypes { 532 blockTypeNames = append(blockTypeNames, name) 533 } 534 sort.Strings(blockTypeNames) 535 for i, name := range blockTypeNames { 536 blockS := schema.BlockTypes[name] 537 538 // We'll disregard any block type that consists only of computed 539 // attributes, since otherwise we'll just create weird empty blocks 540 // that do nothing except create confusion. 541 if !schemaHasSettableArguments(&blockS.Block) { 542 continue 543 } 544 545 if i > 0 || len(attrNames) > 0 { 546 buf.WriteByte('\n') 547 } 548 printBlockOpen(buf, "dynamic", []string{name}, nil) 549 switch blockS.Nesting { 550 case configschema.NestingMap: 551 printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, {})`, iterName, name)), nil) 552 printAttribute(buf, "labels", []byte(fmt.Sprintf(`[%s.key]`, name)), nil) 553 case configschema.NestingSingle, configschema.NestingGroup: 554 printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, null) != null ? [%s.value.%s] : []`, iterName, name, iterName, name)), nil) 555 default: 556 printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, [])`, iterName, name)), nil) 557 } 558 printBlockOpen(buf, "content", nil, nil) 559 moreDiags := printDynamicBlockBody(buf, name, &blockS.Block) 560 diags = diags.Append(moreDiags) 561 buf.WriteString("}\n") 562 buf.WriteString("}\n") 563 } 564 565 return diags 566 } 567 568 func printComments(buf *bytes.Buffer, group *hcl1ast.CommentGroup) { 569 if group == nil { 570 return 571 } 572 for _, comment := range group.List { 573 buf.WriteString(comment.Text) 574 buf.WriteByte('\n') 575 } 576 } 577 578 func printBlockOpen(buf *bytes.Buffer, blockType string, labels []string, commentGroup *hcl1ast.CommentGroup) { 579 buf.WriteString(blockType) 580 for _, label := range labels { 581 buf.WriteByte(' ') 582 printQuotedString(buf, label) 583 } 584 buf.WriteString(" {") 585 if commentGroup != nil { 586 for _, c := range commentGroup.List { 587 buf.WriteByte(' ') 588 buf.WriteString(c.Text) 589 } 590 } 591 buf.WriteByte('\n') 592 } 593 594 func printAttribute(buf *bytes.Buffer, name string, valSrc []byte, commentGroup *hcl1ast.CommentGroup) { 595 buf.WriteString(name) 596 buf.WriteString(" = ") 597 buf.Write(valSrc) 598 if commentGroup != nil { 599 for _, c := range commentGroup.List { 600 buf.WriteByte(' ') 601 buf.WriteString(c.Text) 602 } 603 } 604 buf.WriteByte('\n') 605 } 606 607 func printQuotedString(buf *bytes.Buffer, val string) { 608 buf.WriteByte('"') 609 printStringLiteralFromHILOutput(buf, val) 610 buf.WriteByte('"') 611 } 612 613 func printStringLiteralFromHILOutput(buf *bytes.Buffer, val string) { 614 val = strings.Replace(val, `\`, `\\`, -1) 615 val = strings.Replace(val, `"`, `\"`, -1) 616 val = strings.Replace(val, "\n", `\n`, -1) 617 val = strings.Replace(val, "\r", `\r`, -1) 618 val = strings.Replace(val, `${`, `$${`, -1) 619 val = strings.Replace(val, `%{`, `%%{`, -1) 620 buf.WriteString(val) 621 } 622 623 func printHeredocLiteralFromHILOutput(buf *bytes.Buffer, val string) { 624 val = strings.Replace(val, `${`, `$${`, -1) 625 val = strings.Replace(val, `%{`, `%%{`, -1) 626 buf.WriteString(val) 627 } 628 629 func collectAdhocComments(f *hcl1ast.File) *commentQueue { 630 comments := make(map[hcl1token.Pos]*hcl1ast.CommentGroup) 631 for _, c := range f.Comments { 632 comments[c.Pos()] = c 633 } 634 635 // We'll remove from our map any comments that are attached to specific 636 // nodes as lead or line comments, since we'll find those during our 637 // walk anyway. 638 hcl1ast.Walk(f, func(nn hcl1ast.Node) (hcl1ast.Node, bool) { 639 switch t := nn.(type) { 640 case *hcl1ast.LiteralType: 641 if t.LeadComment != nil { 642 for _, comment := range t.LeadComment.List { 643 delete(comments, comment.Pos()) 644 } 645 } 646 647 if t.LineComment != nil { 648 for _, comment := range t.LineComment.List { 649 delete(comments, comment.Pos()) 650 } 651 } 652 case *hcl1ast.ObjectItem: 653 if t.LeadComment != nil { 654 for _, comment := range t.LeadComment.List { 655 delete(comments, comment.Pos()) 656 } 657 } 658 659 if t.LineComment != nil { 660 for _, comment := range t.LineComment.List { 661 delete(comments, comment.Pos()) 662 } 663 } 664 } 665 666 return nn, true 667 }) 668 669 if len(comments) == 0 { 670 var ret commentQueue 671 return &ret 672 } 673 674 ret := make([]*hcl1ast.CommentGroup, 0, len(comments)) 675 for _, c := range comments { 676 ret = append(ret, c) 677 } 678 sort.Slice(ret, func(i, j int) bool { 679 return ret[i].Pos().Before(ret[j].Pos()) 680 }) 681 queue := commentQueue(ret) 682 return &queue 683 } 684 685 type commentQueue []*hcl1ast.CommentGroup 686 687 func (q *commentQueue) TakeBeforeToken(token hcl1token.Token) []*hcl1ast.CommentGroup { 688 return q.TakeBeforePos(token.Pos) 689 } 690 691 func (q *commentQueue) TakeBefore(node hcl1ast.Node) []*hcl1ast.CommentGroup { 692 return q.TakeBeforePos(node.Pos()) 693 } 694 695 func (q *commentQueue) TakeBeforePos(pos hcl1token.Pos) []*hcl1ast.CommentGroup { 696 toPos := pos 697 var i int 698 for i = 0; i < len(*q); i++ { 699 if (*q)[i].Pos().After(toPos) { 700 break 701 } 702 } 703 if i == 0 { 704 return nil 705 } 706 707 ret := (*q)[:i] 708 *q = (*q)[i:] 709 710 return ret 711 } 712 713 // hcl1NodeEndPos tries to find the latest possible position in the given 714 // node. This is primarily to try to find the last line number of a multi-line 715 // construct and is a best-effort sort of thing because HCL1 only tracks 716 // start positions for tokens and has no generalized way to find the full 717 // range for a single node. 718 func hcl1NodeEndPos(node hcl1ast.Node) hcl1token.Pos { 719 switch tn := node.(type) { 720 case *hcl1ast.ObjectItem: 721 if tn.LineComment != nil && len(tn.LineComment.List) > 0 { 722 return tn.LineComment.List[len(tn.LineComment.List)-1].Start 723 } 724 return hcl1NodeEndPos(tn.Val) 725 case *hcl1ast.ListType: 726 return tn.Rbrack 727 case *hcl1ast.ObjectType: 728 return tn.Rbrace 729 default: 730 // If all else fails, we'll just return the position of what we were given. 731 return tn.Pos() 732 } 733 } 734 735 func hcl1ErrSubjectRange(filename string, err error) *hcl2.Range { 736 if pe, isPos := err.(*hcl1parser.PosError); isPos { 737 return hcl1PosRange(filename, pe.Pos).Ptr() 738 } 739 return nil 740 } 741 742 func hcl1PosRange(filename string, pos hcl1token.Pos) hcl2.Range { 743 return hcl2.Range{ 744 Filename: filename, 745 Start: hcl2.Pos{ 746 Line: pos.Line, 747 Column: pos.Column, 748 Byte: pos.Offset, 749 }, 750 End: hcl2.Pos{ 751 Line: pos.Line, 752 Column: pos.Column, 753 Byte: pos.Offset, 754 }, 755 } 756 } 757 758 func passthruBlockTodo(w io.Writer, node hcl1ast.Node, msg string) { 759 fmt.Fprintf(w, "\n# TF-UPGRADE-TODO: %s\n", msg) 760 hcl1printer.Fprint(w, node) 761 w.Write([]byte{'\n', '\n'}) 762 } 763 764 func schemaHasSettableArguments(schema *configschema.Block) bool { 765 for _, attrS := range schema.Attributes { 766 if attrS.Optional || attrS.Required { 767 return true 768 } 769 } 770 for _, blockS := range schema.BlockTypes { 771 if schemaHasSettableArguments(&blockS.Block) { 772 return true 773 } 774 } 775 return false 776 } 777 778 func invalidLabel(name string) bool { 779 matched, err := regexp.Match(`[0-9]`, []byte{name[0]}) 780 if err == nil { 781 return matched 782 } 783 // This isn't likely, but if there's an error here we'll just ignore it and 784 // move on. 785 return false 786 } 787 788 func printLabelTodo(buf *bytes.Buffer, label string) { 789 buf.WriteString("# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to begin a\n" + 790 "# resource name with a number, but it is no longer possible in Terraform v0.12.\n" + 791 "#\n" + 792 "# Rename the resource and run `terraform state mv` to apply the rename in the\n" + 793 "# state. Detailed information on the `state move` command can be found in the\n" + 794 "# documentation online: https://www.terraform.io/docs/commands/state/mv.html\n", 795 ) 796 }