github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/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/hashicorp/terraform-plugin-sdk/internal/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: ModuleCallInstance{ 285 Call: ModuleCall{ 286 Name: "foo", 287 }, 288 }, 289 SourceRange: tfdiags.SourceRange{ 290 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 291 End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, 292 }, 293 }, 294 ``, 295 }, 296 { 297 `module.foo.bar`, 298 &Reference{ 299 Subject: ModuleCallOutput{ 300 Call: ModuleCallInstance{ 301 Call: ModuleCall{ 302 Name: "foo", 303 }, 304 }, 305 Name: "bar", 306 }, 307 SourceRange: tfdiags.SourceRange{ 308 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 309 End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, 310 }, 311 }, 312 ``, 313 }, 314 { 315 `module.foo.bar.baz`, 316 &Reference{ 317 Subject: ModuleCallOutput{ 318 Call: ModuleCallInstance{ 319 Call: ModuleCall{ 320 Name: "foo", 321 }, 322 }, 323 Name: "bar", 324 }, 325 SourceRange: tfdiags.SourceRange{ 326 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 327 End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, 328 }, 329 Remaining: hcl.Traversal{ 330 hcl.TraverseAttr{ 331 Name: "baz", 332 SrcRange: hcl.Range{ 333 Start: hcl.Pos{Line: 1, Column: 15, Byte: 14}, 334 End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, 335 }, 336 }, 337 }, 338 }, 339 ``, 340 }, 341 { 342 `module.foo["baz"]`, 343 &Reference{ 344 Subject: ModuleCallInstance{ 345 Call: ModuleCall{ 346 Name: "foo", 347 }, 348 Key: StringKey("baz"), 349 }, 350 SourceRange: tfdiags.SourceRange{ 351 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 352 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 353 }, 354 }, 355 ``, 356 }, 357 { 358 `module.foo["baz"].bar`, 359 &Reference{ 360 Subject: ModuleCallOutput{ 361 Call: ModuleCallInstance{ 362 Call: ModuleCall{ 363 Name: "foo", 364 }, 365 Key: StringKey("baz"), 366 }, 367 Name: "bar", 368 }, 369 SourceRange: tfdiags.SourceRange{ 370 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 371 End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21}, 372 }, 373 }, 374 ``, 375 }, 376 { 377 `module.foo["baz"].bar.boop`, 378 &Reference{ 379 Subject: ModuleCallOutput{ 380 Call: ModuleCallInstance{ 381 Call: ModuleCall{ 382 Name: "foo", 383 }, 384 Key: StringKey("baz"), 385 }, 386 Name: "bar", 387 }, 388 SourceRange: tfdiags.SourceRange{ 389 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 390 End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21}, 391 }, 392 Remaining: hcl.Traversal{ 393 hcl.TraverseAttr{ 394 Name: "boop", 395 SrcRange: hcl.Range{ 396 Start: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 397 End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, 398 }, 399 }, 400 }, 401 }, 402 ``, 403 }, 404 { 405 `module`, 406 nil, 407 `The "module" object cannot be accessed directly. Instead, access one of its attributes.`, 408 }, 409 { 410 `module["foo"]`, 411 nil, 412 `The "module" object does not support this operation.`, 413 }, 414 415 // path 416 { 417 `path.module`, 418 &Reference{ 419 Subject: PathAttr{ 420 Name: "module", 421 }, 422 SourceRange: tfdiags.SourceRange{ 423 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 424 End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, 425 }, 426 }, 427 ``, 428 }, 429 { 430 `path.module.blah`, 431 &Reference{ 432 Subject: PathAttr{ 433 Name: "module", 434 }, 435 SourceRange: tfdiags.SourceRange{ 436 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 437 End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, 438 }, 439 Remaining: hcl.Traversal{ 440 hcl.TraverseAttr{ 441 Name: "blah", 442 SrcRange: hcl.Range{ 443 Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 444 End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, 445 }, 446 }, 447 }, 448 }, 449 ``, // valid at this layer, but will fail during eval because "module" is a string 450 }, 451 { 452 `path`, 453 nil, 454 `The "path" object cannot be accessed directly. Instead, access one of its attributes.`, 455 }, 456 { 457 `path["module"]`, 458 nil, 459 `The "path" object does not support this operation.`, 460 }, 461 462 // self 463 { 464 `self`, 465 &Reference{ 466 Subject: Self, 467 SourceRange: tfdiags.SourceRange{ 468 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 469 End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4}, 470 }, 471 }, 472 ``, 473 }, 474 { 475 `self.blah`, 476 &Reference{ 477 Subject: Self, 478 SourceRange: tfdiags.SourceRange{ 479 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 480 End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4}, 481 }, 482 Remaining: hcl.Traversal{ 483 hcl.TraverseAttr{ 484 Name: "blah", 485 SrcRange: hcl.Range{ 486 Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 487 End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 488 }, 489 }, 490 }, 491 }, 492 ``, 493 }, 494 495 // terraform 496 { 497 `terraform.workspace`, 498 &Reference{ 499 Subject: TerraformAttr{ 500 Name: "workspace", 501 }, 502 SourceRange: tfdiags.SourceRange{ 503 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 504 End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19}, 505 }, 506 }, 507 ``, 508 }, 509 { 510 `terraform.workspace.blah`, 511 &Reference{ 512 Subject: TerraformAttr{ 513 Name: "workspace", 514 }, 515 SourceRange: tfdiags.SourceRange{ 516 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 517 End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19}, 518 }, 519 Remaining: hcl.Traversal{ 520 hcl.TraverseAttr{ 521 Name: "blah", 522 SrcRange: hcl.Range{ 523 Start: hcl.Pos{Line: 1, Column: 20, Byte: 19}, 524 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 525 }, 526 }, 527 }, 528 }, 529 ``, // valid at this layer, but will fail during eval because "workspace" is a string 530 }, 531 { 532 `terraform`, 533 nil, 534 `The "terraform" object cannot be accessed directly. Instead, access one of its attributes.`, 535 }, 536 { 537 `terraform["workspace"]`, 538 nil, 539 `The "terraform" object does not support this operation.`, 540 }, 541 542 // var 543 { 544 `var.foo`, 545 &Reference{ 546 Subject: InputVariable{ 547 Name: "foo", 548 }, 549 SourceRange: tfdiags.SourceRange{ 550 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 551 End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7}, 552 }, 553 }, 554 ``, 555 }, 556 { 557 `var.foo.blah`, 558 &Reference{ 559 Subject: InputVariable{ 560 Name: "foo", 561 }, 562 SourceRange: tfdiags.SourceRange{ 563 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 564 End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7}, 565 }, 566 Remaining: hcl.Traversal{ 567 hcl.TraverseAttr{ 568 Name: "blah", 569 SrcRange: hcl.Range{ 570 Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, 571 End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, 572 }, 573 }, 574 }, 575 }, 576 ``, // valid at this layer, but will fail during eval because "module" is a string 577 }, 578 { 579 `var`, 580 nil, 581 `The "var" object cannot be accessed directly. Instead, access one of its attributes.`, 582 }, 583 { 584 `var["foo"]`, 585 nil, 586 `The "var" object does not support this operation.`, 587 }, 588 589 // anything else, interpreted as a managed resource reference 590 { 591 `boop_instance.foo`, 592 &Reference{ 593 Subject: Resource{ 594 Mode: ManagedResourceMode, 595 Type: "boop_instance", 596 Name: "foo", 597 }, 598 SourceRange: tfdiags.SourceRange{ 599 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 600 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 601 }, 602 }, 603 ``, 604 }, 605 { 606 `boop_instance.foo.bar`, 607 &Reference{ 608 Subject: ResourceInstance{ 609 Resource: Resource{ 610 Mode: ManagedResourceMode, 611 Type: "boop_instance", 612 Name: "foo", 613 }, 614 }, 615 SourceRange: tfdiags.SourceRange{ 616 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 617 End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, 618 }, 619 Remaining: hcl.Traversal{ 620 hcl.TraverseAttr{ 621 Name: "bar", 622 SrcRange: hcl.Range{ 623 Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, 624 End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, 625 }, 626 }, 627 }, 628 }, 629 ``, 630 }, 631 { 632 `boop_instance.foo["baz"].bar`, 633 &Reference{ 634 Subject: ResourceInstance{ 635 Resource: Resource{ 636 Mode: ManagedResourceMode, 637 Type: "boop_instance", 638 Name: "foo", 639 }, 640 Key: StringKey("baz"), 641 }, 642 SourceRange: tfdiags.SourceRange{ 643 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 644 End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, 645 }, 646 Remaining: hcl.Traversal{ 647 hcl.TraverseAttr{ 648 Name: "bar", 649 SrcRange: hcl.Range{ 650 Start: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 651 End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, 652 }, 653 }, 654 }, 655 }, 656 ``, 657 }, 658 { 659 `boop_instance.foo["baz"]`, 660 &Reference{ 661 Subject: ResourceInstance{ 662 Resource: Resource{ 663 Mode: ManagedResourceMode, 664 Type: "boop_instance", 665 Name: "foo", 666 }, 667 Key: StringKey("baz"), 668 }, 669 SourceRange: tfdiags.SourceRange{ 670 Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, 671 End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24}, 672 }, 673 }, 674 ``, 675 }, 676 { 677 `boop_instance`, 678 nil, 679 `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 680 }, 681 } 682 683 for _, test := range tests { 684 t.Run(test.Input, func(t *testing.T) { 685 traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1}) 686 if travDiags.HasErrors() { 687 t.Fatal(travDiags.Error()) 688 } 689 690 got, diags := ParseRef(traversal) 691 692 switch len(diags) { 693 case 0: 694 if test.WantErr != "" { 695 t.Fatalf("succeeded; want error: %s", test.WantErr) 696 } 697 case 1: 698 if test.WantErr == "" { 699 t.Fatalf("unexpected diagnostics: %s", diags.Err()) 700 } 701 if got, want := diags[0].Description().Detail, test.WantErr; got != want { 702 t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) 703 } 704 default: 705 t.Fatalf("too many diagnostics: %s", diags.Err()) 706 } 707 708 if diags.HasErrors() { 709 return 710 } 711 712 for _, problem := range deep.Equal(got, test.Want) { 713 t.Errorf(problem) 714 } 715 }) 716 } 717 }