github.com/pulumi/terraform@v1.4.0/pkg/addrs/parse_ref_test.go (about) 1 package addrs 2 3 import ( 4 "testing" 5 6 "github.com/go-test/deep" 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 "github.com/pulumi/terraform/pkg/tfdiags" 10 "github.com/zclconf/go-cty/cty" 11 ) 12 13 func TestParseRef(t *testing.T) { 14 tests := []struct { 15 Input string 16 Want *Reference 17 WantErr string 18 }{ 19 20 // count 21 { 22 `count.index`, 23 &Reference{ 24 Subject: CountAttr{ 25 Name: "index", 26 }, 27 SourceRange: tfdiags.SourceRange{ 28 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 29 End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, 30 }, 31 }, 32 ``, 33 }, 34 { 35 `count.index.blah`, 36 &Reference{ 37 Subject: CountAttr{ 38 Name: "index", 39 }, 40 SourceRange: tfdiags.SourceRange{ 41 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 42 End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, 43 }, 44 Remaining: hcl.Traversal{ 45 hcl.TraverseAttr{ 46 Name: "blah", 47 SrcRange: hcl.Range{ 48 Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 49 End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, 50 }, 51 }, 52 }, 53 }, 54 ``, // valid at this layer, but will fail during eval because "index" is a number 55 }, 56 { 57 `count`, 58 nil, 59 `The "count" object cannot be accessed directly. Instead, access one of its attributes.`, 60 }, 61 { 62 `count["hello"]`, 63 nil, 64 `The "count" object does not support this operation.`, 65 }, 66 67 // each 68 { 69 `each.key`, 70 &Reference{ 71 Subject: ForEachAttr{ 72 Name: "key", 73 }, 74 SourceRange: tfdiags.SourceRange{ 75 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 76 End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8}, 77 }, 78 }, 79 ``, 80 }, 81 { 82 `each.value.blah`, 83 &Reference{ 84 Subject: ForEachAttr{ 85 Name: "value", 86 }, 87 SourceRange: tfdiags.SourceRange{ 88 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 89 End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, 90 }, 91 Remaining: hcl.Traversal{ 92 hcl.TraverseAttr{ 93 Name: "blah", 94 SrcRange: hcl.Range{ 95 Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, 96 End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, 97 }, 98 }, 99 }, 100 }, 101 ``, 102 }, 103 { 104 `each`, 105 nil, 106 `The "each" object cannot be accessed directly. Instead, access one of its attributes.`, 107 }, 108 { 109 `each["hello"]`, 110 nil, 111 `The "each" object does not support this operation.`, 112 }, 113 // data 114 { 115 `data.external.foo`, 116 &Reference{ 117 Subject: Resource{ 118 Mode: DataResourceMode, 119 Type: "external", 120 Name: "foo", 121 }, 122 SourceRange: tfdiags.SourceRange{ 123 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 124 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 125 }, 126 }, 127 ``, 128 }, 129 { 130 `data.external.foo.bar`, 131 &Reference{ 132 Subject: ResourceInstance{ 133 Resource: Resource{ 134 Mode: DataResourceMode, 135 Type: "external", 136 Name: "foo", 137 }, 138 }, 139 SourceRange: tfdiags.SourceRange{ 140 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 141 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 142 }, 143 Remaining: hcl.Traversal{ 144 hcl.TraverseAttr{ 145 Name: "bar", 146 SrcRange: hcl.Range{ 147 Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 148 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 149 }, 150 }, 151 }, 152 }, 153 ``, 154 }, 155 { 156 `data.external.foo["baz"].bar`, 157 &Reference{ 158 Subject: ResourceInstance{ 159 Resource: Resource{ 160 Mode: DataResourceMode, 161 Type: "external", 162 Name: "foo", 163 }, 164 Key: StringKey("baz"), 165 }, 166 SourceRange: tfdiags.SourceRange{ 167 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 168 End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, 169 }, 170 Remaining: hcl.Traversal{ 171 hcl.TraverseAttr{ 172 Name: "bar", 173 SrcRange: hcl.Range{ 174 Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 175 End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, 176 }, 177 }, 178 }, 179 }, 180 ``, 181 }, 182 { 183 `data.external.foo["baz"]`, 184 &Reference{ 185 Subject: ResourceInstance{ 186 Resource: Resource{ 187 Mode: DataResourceMode, 188 Type: "external", 189 Name: "foo", 190 }, 191 Key: StringKey("baz"), 192 }, 193 SourceRange: tfdiags.SourceRange{ 194 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 195 End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, 196 }, 197 }, 198 ``, 199 }, 200 { 201 `data`, 202 nil, 203 `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 204 }, 205 { 206 `data.external`, 207 nil, 208 `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 209 }, 210 211 // local 212 { 213 `local.foo`, 214 &Reference{ 215 Subject: LocalValue{ 216 Name: "foo", 217 }, 218 SourceRange: tfdiags.SourceRange{ 219 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 220 End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9}, 221 }, 222 }, 223 ``, 224 }, 225 { 226 `local.foo.blah`, 227 &Reference{ 228 Subject: LocalValue{ 229 Name: "foo", 230 }, 231 SourceRange: tfdiags.SourceRange{ 232 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 233 End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9}, 234 }, 235 Remaining: hcl.Traversal{ 236 hcl.TraverseAttr{ 237 Name: "blah", 238 SrcRange: hcl.Range{ 239 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 240 End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 241 }, 242 }, 243 }, 244 }, 245 ``, 246 }, 247 { 248 `local.foo["blah"]`, 249 &Reference{ 250 Subject: LocalValue{ 251 Name: "foo", 252 }, 253 SourceRange: tfdiags.SourceRange{ 254 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 255 End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9}, 256 }, 257 Remaining: hcl.Traversal{ 258 hcl.TraverseIndex{ 259 Key: cty.StringVal("blah"), 260 SrcRange: hcl.Range{ 261 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 262 End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 263 }, 264 }, 265 }, 266 }, 267 ``, 268 }, 269 { 270 `local`, 271 nil, 272 `The "local" object cannot be accessed directly. Instead, access one of its attributes.`, 273 }, 274 { 275 `local["foo"]`, 276 nil, 277 `The "local" object does not support this operation.`, 278 }, 279 280 // module 281 { 282 `module.foo`, 283 &Reference{ 284 Subject: ModuleCall{ 285 Name: "foo", 286 }, 287 SourceRange: tfdiags.SourceRange{ 288 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 289 End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, 290 }, 291 }, 292 ``, 293 }, 294 { 295 `module.foo.bar`, 296 &Reference{ 297 Subject: ModuleCallInstanceOutput{ 298 Call: ModuleCallInstance{ 299 Call: ModuleCall{ 300 Name: "foo", 301 }, 302 }, 303 Name: "bar", 304 }, 305 SourceRange: tfdiags.SourceRange{ 306 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 307 End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, 308 }, 309 }, 310 ``, 311 }, 312 { 313 `module.foo.bar.baz`, 314 &Reference{ 315 Subject: ModuleCallInstanceOutput{ 316 Call: ModuleCallInstance{ 317 Call: ModuleCall{ 318 Name: "foo", 319 }, 320 }, 321 Name: "bar", 322 }, 323 SourceRange: tfdiags.SourceRange{ 324 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 325 End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, 326 }, 327 Remaining: hcl.Traversal{ 328 hcl.TraverseAttr{ 329 Name: "baz", 330 SrcRange: hcl.Range{ 331 Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 332 End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, 333 }, 334 }, 335 }, 336 }, 337 ``, 338 }, 339 { 340 `module.foo["baz"]`, 341 &Reference{ 342 Subject: ModuleCallInstance{ 343 Call: ModuleCall{ 344 Name: "foo", 345 }, 346 Key: StringKey("baz"), 347 }, 348 SourceRange: tfdiags.SourceRange{ 349 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 350 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 351 }, 352 }, 353 ``, 354 }, 355 { 356 `module.foo["baz"].bar`, 357 &Reference{ 358 Subject: ModuleCallInstanceOutput{ 359 Call: ModuleCallInstance{ 360 Call: ModuleCall{ 361 Name: "foo", 362 }, 363 Key: StringKey("baz"), 364 }, 365 Name: "bar", 366 }, 367 SourceRange: tfdiags.SourceRange{ 368 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 369 End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21}, 370 }, 371 }, 372 ``, 373 }, 374 { 375 `module.foo["baz"].bar.boop`, 376 &Reference{ 377 Subject: ModuleCallInstanceOutput{ 378 Call: ModuleCallInstance{ 379 Call: ModuleCall{ 380 Name: "foo", 381 }, 382 Key: StringKey("baz"), 383 }, 384 Name: "bar", 385 }, 386 SourceRange: tfdiags.SourceRange{ 387 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 388 End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21}, 389 }, 390 Remaining: hcl.Traversal{ 391 hcl.TraverseAttr{ 392 Name: "boop", 393 SrcRange: hcl.Range{ 394 Start: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 395 End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, 396 }, 397 }, 398 }, 399 }, 400 ``, 401 }, 402 { 403 `module`, 404 nil, 405 `The "module" object cannot be accessed directly. Instead, access one of its attributes.`, 406 }, 407 { 408 `module["foo"]`, 409 nil, 410 `The "module" object does not support this operation.`, 411 }, 412 413 // path 414 { 415 `path.module`, 416 &Reference{ 417 Subject: PathAttr{ 418 Name: "module", 419 }, 420 SourceRange: tfdiags.SourceRange{ 421 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 422 End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, 423 }, 424 }, 425 ``, 426 }, 427 { 428 `path.module.blah`, 429 &Reference{ 430 Subject: PathAttr{ 431 Name: "module", 432 }, 433 SourceRange: tfdiags.SourceRange{ 434 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 435 End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, 436 }, 437 Remaining: hcl.Traversal{ 438 hcl.TraverseAttr{ 439 Name: "blah", 440 SrcRange: hcl.Range{ 441 Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 442 End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, 443 }, 444 }, 445 }, 446 }, 447 ``, // valid at this layer, but will fail during eval because "module" is a string 448 }, 449 { 450 `path`, 451 nil, 452 `The "path" object cannot be accessed directly. Instead, access one of its attributes.`, 453 }, 454 { 455 `path["module"]`, 456 nil, 457 `The "path" object does not support this operation.`, 458 }, 459 460 // self 461 { 462 `self`, 463 &Reference{ 464 Subject: Self, 465 SourceRange: tfdiags.SourceRange{ 466 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 467 End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4}, 468 }, 469 }, 470 ``, 471 }, 472 { 473 `self.blah`, 474 &Reference{ 475 Subject: Self, 476 SourceRange: tfdiags.SourceRange{ 477 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 478 End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4}, 479 }, 480 Remaining: hcl.Traversal{ 481 hcl.TraverseAttr{ 482 Name: "blah", 483 SrcRange: hcl.Range{ 484 Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 485 End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 486 }, 487 }, 488 }, 489 }, 490 ``, 491 }, 492 493 // terraform 494 { 495 `terraform.workspace`, 496 &Reference{ 497 Subject: TerraformAttr{ 498 Name: "workspace", 499 }, 500 SourceRange: tfdiags.SourceRange{ 501 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 502 End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19}, 503 }, 504 }, 505 ``, 506 }, 507 { 508 `terraform.workspace.blah`, 509 &Reference{ 510 Subject: TerraformAttr{ 511 Name: "workspace", 512 }, 513 SourceRange: tfdiags.SourceRange{ 514 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 515 End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19}, 516 }, 517 Remaining: hcl.Traversal{ 518 hcl.TraverseAttr{ 519 Name: "blah", 520 SrcRange: hcl.Range{ 521 Start: hcl.Pos{Line: 1, Column: 20, Byte: 19}, 522 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 523 }, 524 }, 525 }, 526 }, 527 ``, // valid at this layer, but will fail during eval because "workspace" is a string 528 }, 529 { 530 `terraform`, 531 nil, 532 `The "terraform" object cannot be accessed directly. Instead, access one of its attributes.`, 533 }, 534 { 535 `terraform["workspace"]`, 536 nil, 537 `The "terraform" object does not support this operation.`, 538 }, 539 540 // var 541 { 542 `var.foo`, 543 &Reference{ 544 Subject: InputVariable{ 545 Name: "foo", 546 }, 547 SourceRange: tfdiags.SourceRange{ 548 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 549 End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7}, 550 }, 551 }, 552 ``, 553 }, 554 { 555 `var.foo.blah`, 556 &Reference{ 557 Subject: InputVariable{ 558 Name: "foo", 559 }, 560 SourceRange: tfdiags.SourceRange{ 561 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 562 End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7}, 563 }, 564 Remaining: hcl.Traversal{ 565 hcl.TraverseAttr{ 566 Name: "blah", 567 SrcRange: hcl.Range{ 568 Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 569 End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, 570 }, 571 }, 572 }, 573 }, 574 ``, // valid at this layer, but will fail during eval because "module" is a string 575 }, 576 { 577 `var`, 578 nil, 579 `The "var" object cannot be accessed directly. Instead, access one of its attributes.`, 580 }, 581 { 582 `var["foo"]`, 583 nil, 584 `The "var" object does not support this operation.`, 585 }, 586 587 // the "resource" prefix forces interpreting the next name as a 588 // resource type name. This is an alias for just using a resource 589 // type name at the top level, to be used only if a later edition 590 // of the Terraform language introduces a new reserved word that 591 // overlaps with a resource type name. 592 { 593 `resource.boop_instance.foo`, 594 &Reference{ 595 Subject: Resource{ 596 Mode: ManagedResourceMode, 597 Type: "boop_instance", 598 Name: "foo", 599 }, 600 SourceRange: tfdiags.SourceRange{ 601 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 602 End: tfdiags.SourcePos{Line: 1, Column: 27, Byte: 26}, 603 }, 604 }, 605 ``, 606 }, 607 608 // We have some names reserved which might be used by a 609 // still-under-discussion proposal for template values or lazy 610 // expressions. 611 { 612 `template.foo`, 613 nil, 614 `The symbol name "template" is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix "resource." to force interpretation as a resource type name.`, 615 }, 616 { 617 `lazy.foo`, 618 nil, 619 `The symbol name "lazy" is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix "resource." to force interpretation as a resource type name.`, 620 }, 621 { 622 `arg.foo`, 623 nil, 624 `The symbol name "arg" is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix "resource." to force interpretation as a resource type name.`, 625 }, 626 627 // anything else, interpreted as a managed resource reference 628 { 629 `boop_instance.foo`, 630 &Reference{ 631 Subject: Resource{ 632 Mode: ManagedResourceMode, 633 Type: "boop_instance", 634 Name: "foo", 635 }, 636 SourceRange: tfdiags.SourceRange{ 637 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 638 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 639 }, 640 }, 641 ``, 642 }, 643 { 644 `boop_instance.foo.bar`, 645 &Reference{ 646 Subject: ResourceInstance{ 647 Resource: Resource{ 648 Mode: ManagedResourceMode, 649 Type: "boop_instance", 650 Name: "foo", 651 }, 652 }, 653 SourceRange: tfdiags.SourceRange{ 654 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 655 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 656 }, 657 Remaining: hcl.Traversal{ 658 hcl.TraverseAttr{ 659 Name: "bar", 660 SrcRange: hcl.Range{ 661 Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 662 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 663 }, 664 }, 665 }, 666 }, 667 ``, 668 }, 669 { 670 `boop_instance.foo["baz"].bar`, 671 &Reference{ 672 Subject: ResourceInstance{ 673 Resource: Resource{ 674 Mode: ManagedResourceMode, 675 Type: "boop_instance", 676 Name: "foo", 677 }, 678 Key: StringKey("baz"), 679 }, 680 SourceRange: tfdiags.SourceRange{ 681 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 682 End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, 683 }, 684 Remaining: hcl.Traversal{ 685 hcl.TraverseAttr{ 686 Name: "bar", 687 SrcRange: hcl.Range{ 688 Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 689 End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, 690 }, 691 }, 692 }, 693 }, 694 ``, 695 }, 696 { 697 `boop_instance.foo["baz"]`, 698 &Reference{ 699 Subject: ResourceInstance{ 700 Resource: Resource{ 701 Mode: ManagedResourceMode, 702 Type: "boop_instance", 703 Name: "foo", 704 }, 705 Key: StringKey("baz"), 706 }, 707 SourceRange: tfdiags.SourceRange{ 708 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 709 End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, 710 }, 711 }, 712 ``, 713 }, 714 { 715 `boop_instance`, 716 nil, 717 `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 718 }, 719 } 720 721 for _, test := range tests { 722 t.Run(test.Input, func(t *testing.T) { 723 traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1}) 724 if travDiags.HasErrors() { 725 t.Fatal(travDiags.Error()) 726 } 727 728 got, diags := ParseRef(traversal) 729 730 switch len(diags) { 731 case 0: 732 if test.WantErr != "" { 733 t.Fatalf("succeeded; want error: %s", test.WantErr) 734 } 735 case 1: 736 if test.WantErr == "" { 737 t.Fatalf("unexpected diagnostics: %s", diags.Err()) 738 } 739 if got, want := diags[0].Description().Detail, test.WantErr; got != want { 740 t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) 741 } 742 default: 743 t.Fatalf("too many diagnostics: %s", diags.Err()) 744 } 745 746 if diags.HasErrors() { 747 return 748 } 749 750 for _, problem := range deep.Equal(got, test.Want) { 751 t.Errorf(problem) 752 } 753 }) 754 } 755 }